הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

אופס עשיתי את זה שוב

01/08/2023

כן בסיסי נתונים הם חשובים. כן הם צריכים לעבוד מהר וזה אחלה שהם יודעים לנהל המון המון מידע. אבל איכשהו מסע בזמן אף פעם לא היה הצד החזק שלהם.

וככה כשמישהו כותב בטעות:

DELETE FROM users;

ושוכח להוסיף את ה WHERE, או שטועה בתנאי באיזה UPDATE אנחנו נתקעים.

וכן אפשר לחזור מגיבוי וחשוב שיהיו גיבויים, אבל בינינו לחזור מגיבוי זה לא טיול בפארק אפילו כשאתם יודעים מה אתם עושים. וכן אפשר להוציא מגיבוי רק את התוכן של טבלת המשתמשים ואיכשהו לייצא את הנתונים שם לבסיס נתונים נפרד ולייבא אותם בחזרה לבסיס נתונים הראשי, אבל גם זה לא הולך לעבור חלק.

במקום כל אלה הייתי רוצה לראות כלי פשוט שיגרום לבסיס הנתונים להתנהג כמו גיט ביחס למסע בזמן. למשל שאפשר היה לכתוב:

git log -- users;

ולקבל את כל השינויים שבוצעו לטבלת המשתמשים, ואפילו:

git restore -s HEAD~1 users

כדי להחזיר את התוכן של טבלת המשתמשים קומיט אחד אחורה.

עד שימציאו כזה מנגנון שימו לב לגיבויים ונסו להתרחק מפקודות מחיקה.

השנה 2023. הגיע הזמן להפסיק לדבר על ירושה בפייתון

31/07/2023

הקונספט של ירושה בין מחלקות היה פופולרי כשכולם כתבו Java ו C++ בגלל שזו היתה הדרך היחידה לכתוב קוד פולימורפי. אם רצינו ב Java ליצור מערך של מוצרים ממחלקות שונות, היה עלינו לוודא שלכל המחלקות יש Base Class משותף, והשפה עצמה וידאה שאנחנו מתיחסים רק למאפיינים מה Base Class של הדברים מהמערך.

אני מבין את ההגיון הזה ב Java. אבל בפייתון? הנה הקוד למחלקה של עגלת קניות שמכילה 3 סוגים של מוצרים:

from dataclasses import dataclass

class Cart:
  def __init__(self):
    self.products = []

@dataclass
class Table:
  price: float
  name: str

@dataclass
class Shoes:
  price: float
  color: str

c = Cart()
c.products.append(Table(price=120, name="Big Table"))
c.products.append(Table(price=80, name="Small Table"))
c.products.append(Shoes(price=180, color="orange"))

יותר מזה, אני יכול להוסיף פונקציה ל Cart שתדפיס את המחיר הכולל של המוצרים:

class Cart:
  def __init__(self):
    self.products = []

  def price(self):
    return sum(p.price for p in self.products)

ואפשר להגדיר פרוטוקול כדי לבדוק טיפוסים בזמן כתיבת הקוד:

class Product(Protocol):
  price: float

class Cart:
  def __init__(self) -> None:
    self.products: list[Product] = []

  def price(self):
    return sum(p.price for p in self.products)

ואז אפשר לקבל שגיאות כשמנסים להוסיף משהו שאין לו מחיר לעגלת הקניות, בדיוק כמו שהיה קורה ב Java.

תרחיש שני שאולי היה גורם למישהו לחשוב על יצירת עצי ירושה הוא שימוש חוזר בקוד. במקרה שלנו אפשר לדמיין קוד שיהיה משותף לכל המוצרים ולכן כדאי לכתוב אותו רק במחלקת הבסיס "ולרשת" את המימוש במחלקות של שאר המוצרים. לצערי גם כאן כל דוגמה שאני מצליח לחשוב עליה אני יכול לשכתב לקוד טוב יותר תוך שימוש במנגנונים אחרים לשימוש חוזר בקוד של השפה, בדגש על Descriptors ו Class Decorators. החיסרון בירושה הוא שאתה מקבל המון דברים במכה אחת ובלי לבקש אותם, ובלי אפשרות לשלוט באיזה פונקציונאליות בדיוק תקבל.

השנה 2023 וכבר מזמן אין סיבה להשתמש בירושה ב Python.

איך לא ראיתי את זה קודם?

30/07/2023

ב 2008 כשרק יצא האייפון חשבתי שטלפונים הם הדבר הגדול הבא והבנתי שאני צריך להיכנס לתחום. לקחתי חצי שנה הפסקה מהחיים, למדתי את הטכנולוגיה וגם בניתי כמה אפליקציות ... אבל לא לאייפון. כי כשהאייפון רק יצא החברה ששלטה בשוק נקראה נוקיה.

"מי ירצה לפתח אפליקציות לטלפון שאין לאף אחד?"

אחרי אותה חצי שנה מצאתי עבודה טובה בתור מפתח סימביאן באיזו חברה. המשכתי לכתוב אפליקציות עד שהפלטפורמה עלתה באש וארבע שנים מאוחר יותר התחלתי לפתח לאייפון.

"איך לא ראיתי את זה קודם?"

אם הייתי רואה את זה בזמן הייתי יכול לחסוך 4 שנים של לימוד ופיתוח על הפלטפורמה הלא נכונה. אם הייתי רואה את זה בזמן אולי הכל היה נראה אחרת. אולי לא הייתי מפספס את הרכבת.

"איך לא ראיתי את זה קודם" היא שאלה מעניינת אבל לא טובה. היא רק דלת לבור אינסופי של הרהורים ולא עוזרת לנו להתקדם. היא הסחת דעת במסווה של התלבטות רצינית. והיא מסתירה את השאלה החשובה בהרבה - "איך לשים לב טוב יותר בפעם הבאה".

איך לראות בזמן מי הסימביאן של היום, מי הטכנולוגיה שבעוד שנתיים כבר לא תהיה רלוונטית. ואיך להגיב בזמן ולהתחיל היום ללמוד את ה Objective C הבאה. את הדבר שהיום אף אחד לא משתמש בו אבל עוד שנתיים יהיה במרכז העניינים.

"מה עוד אני מפספס?" ו"מה כדאי לנסות היום שעד עכשיו לא היה נראה לי רלוונטי" הן שאלות הרבה יותר טובות. הן פותחות את הדלת, מאחוריהן יש מחקר, יש חיפוש ואולי גם האוצר הבא.

שני סוגים של Outsourcing

29/07/2023

יש את ה Outsourcing של אלה שיודעים ורוצים לחסוך זמן: זה יהיה הצוות שעובד על פיתוח פרויקט בריאקט אבל המעצב שלהם יודע רק פיגמה. הם היו שמחים לייצא את העיצוב מ figma לקומפוננטות ריאקט ולהתחיל לעבוד אבל לא מצאו עדיין כלי מספיק טוב ולכן יעדיפו לשלוח את ה figma לקבוצה חיצונית שתמיר את הכל לקומפוננטות ריאקט כדי שהם יוכלו להתמקד בלוגיקה, בביצועים ובפונקציונאליות של היישום.

ויש את ה Outsourcing של אלה שאין להם מושג: זה יהיה אותו צוות כשיצטרכו להנגיש את האתר. הם לא למדו איך לעשות את זה ואין למנהלים שלהם רצון "לבזבז" זמן על הכשרה. עדיף להוציא החוצה את העבודה לקבוצה שמתמחה בהנגשה ולקבל קוד מתוקן.

אחד חוסך זמן ומשפר את קצב העבודה. השני כמעט תמיד ייכשל בגלל חוסר הבנה ואינטרסים לא מתואמים של הצדדים.

ה Regexp הלא נכון

28/07/2023

באמצע משחק באיזה אתר של ביטויים רגולאריים הם שאלו איך לתפוס דרך ביטוי רגולארי טקסט שמכיל רווחים בשני הצדדים, בלי לתפוס את הרווחים שבהתחלה ובסוף. ההצעה שלהם היתה:

^\s*(.*)\s*$

אני יכול לראות למה זה נראה כמו רעיון טוב - אתה חושב שאתה תופס את הטקסט שבאמצע, ויותר מזה אם ננסה את הביטוי ונדפיס את ה Capture Group הראשון התוצאה "תיראה" נכונה:

$ echo "     hello world    " | perl -nE '/^\s*(.*)\s*$/ && say $1'
hello world

עד שאנחנו שמים לב שחסר משהו בביטוי. תיקון תוכנית הבדיקה כבר יחשוף את הבעיה:

$ echo "     hello world    " | perl -nE '/^\s*(.*)\s*$/ && say "[$1]"' 
[hello world    ]

וזה ברור - החלק של הנקודה כוכבית באמצע תפס את כל הביטוי כולל את כל הרווחים שבסוף. עד שמישהו הגיע לחפש את החלק האחרון של הביטוי כבר לא נשארו רווחים בטקסט. ואחרי שרואים את זה גם הפיתרון ברור, פשוט מוסיפים סימן שאלה אחרי הנקודה כוכבית שבאמצע כדי לקחת "הכי מעט תווים שאפשר" לחלק הזה. התוצאה היא:

$ echo "     hello world    " | perl -nE '/^\s*(.*?)\s*$/ && say "[$1]"'
[hello world]

וכן ביטויים רגולאריים זה מסורבל וכל מה שתגידו נכון, אבל הטיפ שלנו היום הוא יותר כללי מביטויים רגולאריים - כן חשוב לכתוב קוד נכון, אבל אפילו יותר חשוב לכתוב תוכניות בדיקה טובות. כשרואים את הסוגריים המרובעים מסביב לפלט מיד רואים את הטעות ואפשר לתקן אותה.

שישה שלבים של שימוש בספריה חיצונית

27/07/2023

בעבודה עם ספריה חיצונית לאורך זמן אנחנו מטפסים בסולם בן שישה שלבים-

  1. לעשות מה שב Readme.

  2. לחפש עוד דוגמאות ב API Doc.

  3. להיכנס לקוד הספריה כדי להבין טוב יותר איך דברים עובדים.

  4. לחפש Github Issues רלוונטים כדי לפתור בעיות נקודתיות.

  5. לכתוב קוד שיתממשק עם ה Internals של הספריה (ישתמש בפונקציות פרטיות, יעשה Monkey Patch לחלקים שם).

  6. למזלג, ליצור גירסה שלנו ולשלוח Pull Request.

מרגישים שאתם יודעים מספיק על ספריה כלשהי? זה זמן טוב לעלות שלב בסולם.

ושוב mypy הציל לי את היום

26/07/2023

שימו לב לקוד הבא בלי Type Hints:

items = [{'a': 10, 'b': 20, '_id': 1},
         {'a': 12, 'b': 31, '_id': 2}]

x = 5 # type: int

def print_item(items, index):
    i = items[index]
    del(i['_id'])
    print(i)


print_item(items, 0)
print_item(items, 1)

רואים את הבאג? הקוד מדפיס את שני המילונים אבל מקלקל את הרשימה. אם נדפיס את items בסוף התוכנית נקבל:

[{'a': 10, 'b': 20}, {'a': 12, 'b': 31}]

כלומר מאפיין _id נמחק. ברור שזו לא היתה הכוונה בכתיבת הפונקציה. print_item רק צריכה להדפיס פריט בלי השדות המיוחדים שמתחילים בסימן קו תחתי, אבל על הדרך מישהו התבלבל ומחק מידע מרשימה שהתקבלה בתור קלט.

עכשיו אותו הקוד עם Type Hints:

from typing import Mapping

Items = list[Mapping[str, int]]

items: Items = [{'a': 10, 'b': 20, '_id': 1},
                {'a': 12, 'b': 31, '_id': 2}]

x = 5 # type: int

def print_item(items: Items, index: int):
    i = items[index]
    del(i['_id'])
    print(i)


print_item(items, 0)
print_item(items, 1)

print(items)

הפעם כבר בתוך PyCharm ובטח בהפעלת mypy מקבלים את השגיאה:

demo.py:12: error: "Mapping[str, int]" has no attribute "__delitem__"; maybe "__getitem__"?  [attr-defined]
Found 1 error in 1 file (checked 1 source file)

נכון, צריך לזכור להשתמש בטיפוס הנכון (Mapping במקום dict לדברים שאתם לא רוצים שישתנו), וברור שהיה נחמד אם בדיקות כאלה היו קורות בברירת מחדל, ובכל זאת גם זו תזכורת טובה לחשיבות של Type Hints.

הזדמנויות / דברים יותר דחופים

25/07/2023

סדר עדיפויות זה מעולה וחשוב כדי להתקדם בחיים. אבל סדר עדיפויות משקף תמיד מצב קיים. הוא משקף את מה שאת חושבת שאת צריכה. סדר עדיפויות הוא סדר העדיפויות שקבעת אתמול.

סדר עדיפויות לעתים נדירות משאיר מקום להזדמנויות.

נתקלת בהרצאה מקצועית שנראית סופר מעניינת ביוטיוב. היא באורך שעה ולראות אותה בטוח יבוא על חשבון דברים אחרים מהרשימה. אבל לוותר עליה אומר שלעולם היא לא תגיע לרשימה, כי יש דברים אחרים ממש חשובים שכבר נמצאים שם.

המצחיק הוא שאנחנו ממש לא טובים בסינון גנבי זמן אמיתיים: גלילה אינסופית בפייסבוק? תביאו. טיקטוק? ברור. חתול נכנס למכונית ולא תאמינו מה קרה אחר כך? לוחץ בלי להתבלבל. הרצאה מקצועית של שעה? נו, אני אחזור לזה כשיהיה זמן. שזה אף פעם.

הזדמנויות - דברים שאפשר לעשות עכשיו. שמתחשק לך לעשות עכשיו. ושאם לא יקרו עכשיו כבר לא יקרו לעולם. לא כל ההזדמנויות שוות, אבל אם זיהית הזדמנות בריאה חבל לוותר עליה.

ובבקשה תשתמשו ב Chat GPT בתרגילים הבאים

24/07/2023

עכשיו כש Chat GPT הוא חלק מהחיים המקצועיים שלנו. ועכשיו כשאנחנו משתמשים ונשתמש בו בעבודה כל יום, בדיוק כמו בגוגל.

למה אנחנו עדיין חוששים להשתמש בו בקורסים? למה אנחנו עדיין נותנים תרגילים ש Chat GPT יפתור טוב יותר מהתלמידים? איך לא הגיע הזמן לעלות שלב?

מאחר ו Chat GPT לא הכי מבריק בהתמודדות עם מורכבויות האתגר הבא שלנו בתור מרצים יהיה לכתוב תרגילים יותר מורכבים. תרגילים ש Chat GPT יתפקד בהם בתור "כלי עבודה" ולא בתור "נשק סודי שפותר הכל יותר טוב מהתלמיד". אני לא בטוח עדיין איך לעשות את זה, אבל בקורסים הבאים אשחק עם כמה רעיונות.

משפט המפתח - בבקשה תיעזרו ב Chat GPT במהלך העבודה על התרגילים הבאים.

טיפ פייתון: ערך אקראי מתוך Literal

23/07/2023

הגבול בפייתון בין הגדרות טיפוסים לבין קוד זמן ריצה הוא דק מאוד - כל הגדרות הטיפוסים שנגדיר זמינות גם לקוד זמן ריצה (הרבה מהם באמצעות inspect.get_annotations), ואפשר גם להשתמש במשתני טיפוסים ולגשת למאפיינים שלהם.

וכך הטיפ של היום מתחיל בקוד הבא:

from typing import Literal
CountryCode = Literal["il", "al", "ar", "fr"]

קל לדמיין פונקציה שמקבלת קוד מדינה ועושה משהו, אבל מה עושים אם רוצים להמציא קוד מדינה באקראי מתוך הרשימה, או להדפיס למשתמש את רשימת כל הקודים האפשריים? פה הכיף של להשתמש במשתנה טיפוס גם בזמן ריצה. הנה ההמשך:

from typing import Literal
import random
CountryCode = Literal["il", "al", "ar", "fr"]

def get_country_name(code: CountryCode):
    pass

random_country_code = random.choice(CountryCode.__args__)

ומה לגבי התרחיש ההפוך? נניח שהיה לנו מילון שהמפתחות שלו הם הקודים של המדינות והיינו רוצים להשתמש במילון זה בזמן בדיקת הטיפוסים, בסגנון keyof של TypeScript? לצערי אני לא מכיר מנגנון כזה בפייתון, אבל אם אתם מכירים אל תתביישו וספרו לי בתגובות.