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

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

חדש באתר: מיני קורס Type Hints בפייתון

04/08/2023

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

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

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

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

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

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

  3. איך (ולמה) לעבוד עם TypeVar.

  4. איך להגדיר פרוטוקול ומתי להשתמש בו, ואיזה פרוטוקולים כבר מוגדרים בשפה.

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

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

מנויים לאתר יכולים כבר לצפות בתוכן בקישור: https://www.tocode.co.il/boosters/13.

ואם אתם עדיין לא מנויים היום הוא הזדמנות מצוינת להירשם.

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

עד השיעור הבא

03/08/2023

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

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

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

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

כמה סיבות לשפר קוד שעובד

02/08/2023

קוד הבדיקה הבא ב PyTest עובד מעולה (גם אם לא ממש ברור למה הוא טוב כי זו בדיקה מומצאת רק בשביל הפוסט):

from unittest.mock import MagicMock

def test_mock(monkeypatch):
    hello = MagicMock()
    fake_len = MagicMock()
    fake_len.return_value = 8
    hello.__len__ = fake_len

    with monkeypatch.context() as m:
        m.setattr(hello, '__len__', fake_len)
        assert len(hello) == 8
        assert fake_len.called

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

def test_mock(monkeypatch):
    hello = MagicMock()
    hello.__len__ = MagicMock(return_value=8)

    monkeypatch.setattr(hello, '__len__', hello.__len__)
    assert len(hello) == 8
    assert hello.__len__.called

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

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

  1. בשביל לתקן אני צריך להבין מה עושה monkeypatch.context ומתי כן צריך להשתמש בו. רק להבין את זה שווה את המאמץ, בלי קשר לקוד שיתקבל.

  2. קוד גרוע משתכפל ומייצר Cargo Cults - לאורך זמן אני יכול לדמיין עשרות ומאות בדיקות שישתמשו ב context, אפילו שאין בזה שום צורך. זה מבזבז זמן ובמיוחד אנחנו רואים את זה ב Code Reviews כשמתכנתת חדשה נכנסת לצוות וצריכה ללמוד את כל החוקים המשונים של הצוות שאין להם חשיבות טכנית.

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

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

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

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.