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

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

מצאו את ההבדלים

01/12/2021

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

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

(בגיט יש מנגנון מובנה בשם git bisect שבנוי בדיוק על הרעיון הזה)

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

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

חלומות על עננים

30/11/2021

לפני פחות מעשרים שנה לשרתי יוניקס עדיין היתה נוכחות בחיים של מפתחי תוכנה. בחברות רבות היה "חדר שרתים" בו איחסנו שרתי יוניקס שלמים, וחברות אחרות היו שוכרות שרת שלם שישב באיזו חוות שרתים. אני אישית עוד זכיתי לעבוד על מכונת HP True 64 שיושבת בתוך ארון בחדר בהמשך המזדרון.

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

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

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

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

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

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

  4. אנחנו צריכים דרך לשתף spec שלם של מערכת על ענן. משהו כמו helm chart או docker compose עבור כל אותם אינסוף ממשקים של AWS או Azure או GCP או מה שלא תרצו.

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

ומה אתכם? איזה חלומות יש לכם על הענן?

קיים בשפה

29/11/2021

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

a = [1, 2, 3]
b = ['a', 'b', 'c']
c = ['@', '!', '#']

אז הפעלה של zip על שלושת הרשימות תחזיר רשימה אחת של שלושה איברים שכל אחד מהם מורכב מאיבר אחד מכל אחת מהרשימות, כלומר:

>>> list(zip(a, b, c))
[(1, 'a', '@'), (2, 'b', '!'), (3, 'c', '#')]

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

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

a = [1, 2, 3, 4, 5, 6]
b = ['a', 'b', 'c']
c = ['@', '!', '#']

אז התוצאה של ה zip תישאר בדיוק אותו דבר - והאיברים העודפים ב a פשוט לא יקבלו טיפול.

עכשיו תשמעו מה היה לבראנט בושר להגיד על זה:

It is clear from the author's personal experience and a survey of the standard library that much (if not most) zip usage involves iterables that must be of equal length.

ובהמשך אותו מסמך:

In fact, the author has counted dozens of other call sites in Python's standard library and tooling where it would be appropriate to enable this new feature immediately.

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

ופה יש שני דברים שצריך לשים לב אליהם:

  1. כל אחד היה יכול לעטוף את zip בפונקציה חדשה (נקרא לה safezip) שבודקת שאכן כל רשימות הקלט באותו האורך. מעטים אם בכלל עשו את זה.

  2. (וזה ניחוש שלי) - אחרי התוספת ל zip בפייתון 3.10, הרבה אנשים יתחילו להעביר את הפרמטר החדש ואפילו יסתכלו מוזר על אלה שלא מעבירים אותו.

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

עיקרון הרצף

27/11/2021

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

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

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

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

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

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

פייתון ומפתחות בלי מרכאות

26/11/2021

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

filters = [
        { id: 12, filter: None },
        { id: 15, filter: None },
        { id: 16, filter: lambda x: x > 7 }
        ]

def run_filter(filter_id, data):
    fn = next(x for x in filters if x.get("id") == filter_id)["filter"]
    return [x for x in data if fn(x)]

print(run_filter(16, [2, 5, 9, 11, 20]))

במקום להפעיל את פילטר מספר 16 הקוד מתרסק עם השגיאה הבאה:

Traceback (most recent call last):
  File "/Users/ynonp/tmp/blog/c.py", line 11, in <module>
    print(run_filter(16, [2, 5, 9, 11, 20]))
  File "/Users/ynonp/tmp/blog/c.py", line 8, in run_filter
    fn = next(x for x in filters if x.get("id") == filter_id)["filter"]
StopIteration

שאומרת ש next נכשל כי הוא לא מצא אף פילטר שמתאים לתנאי. זה מוזר כי אני די בטוח שהוגדר פילטר שה id שלו הוא 16. כרגע הקוד לא כולל Type Hints ולכן גם mypy לא מתלונן.

נוסיף Type Hints ונראה אם זה יעזור לקבל יותר מידע:

from collections.abc import Callable
from typing import TypedDict 

class Filter(TypedDict):
    id: int
    filter: Callable[[int], bool]|None

filters: list[Filter] = [
        { id: 12, filter: None },
        { id: 15, filter: None },
        { id: 16, filter: lambda x: x > 7 }
        ]

def run_filter(filter_id, data):
    fn = next(x for x in filters if x.get("id") == filter_id)["filter"]
    return [x for x in data if fn(x)]

print(run_filter(16, [2, 5, 9, 11, 20]))

התוכנית עדיין מתרסקת אבל עכשיו לפחות mypy מצליח למצוא בעיות:

$ mypy b.py

b.py:9: error: Expected TypedDict key to be string literal
b.py:10: error: Expected TypedDict key to be string literal
b.py:11: error: Expected TypedDict key to be string literal
Found 3 errors in 1 file (checked 1 source file)

מבט ממוקד יותר בשורה 9 חושף את הבעיה:

{ id: 12, filter: None },

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

[{<built-in function id>: 12, <class 'filter'>: None}, {<built-in function id>: 15, <class 'filter'>: None}, {<built-in function id>: 16, <class 'filter'>: <function <lambda> at 0x1032b3d90>}]

וכמו תמיד כשמבינים את הבעיה התיקון הוא החלק הקל:

from collections.abc import Callable
from typing import TypedDict 

class Filter(TypedDict):
    id: int
    filter: Callable[[int], bool]|None

filters: list[Filter] = [
        { "id": 12, "filter": None },
        { "id": 15, "filter": None },
        { "id": 16, "filter": lambda x: x > 7 }
        ]

def run_filter(filter_id, data):
    fn = next(x for x in filters if x["id"] == filter_id)["filter"]
    return [x for x in data if fn(x)]

print(run_filter(16, [2, 5, 9, 11, 20]))

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

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

מהיר, עמיד ומתועד

25/11/2021

שלוש מילים שאומרות הכל על הפיתרון הבא שעליך לכתוב:

מהיר- כי Over Engineering זו בעיה; כי יש לקוח שמחכה; כי הזמן שלך יקר ואין טעם לבזבז אותו על סיבוכים מיותרים.

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

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

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

אני רוצה להיות ...

24/11/2021

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

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

ויש המון סיבות לפער:

  1. תכף יש דמו גדול, אחרי ההשקעה כבר נוכל לבנות מוצר כמו שצריך.

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

  3. יש עכשיו המון לקוחות חדשים, צריך לתקן מהר את הבעיות כדי לא ליצור לחברה שם רע.

  4. אין לנו לקוחות בכלל אז חייבים לבנות מהר את הפיצ'ר הזה. אחרי שנמצא כמה לקוחות נוכל לתקן.

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

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

מעניין, מועיל, חשוב

22/11/2021

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

היכרות טובה עם כלי עבודה שאתם כבר היום משתמשים בו יכולה מאוד להועיל לפרודוקטיביות שלכם.

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

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

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