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

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

קבלת החלטות בתנאי אי וודאות

22/09/2023

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

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

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

כמה דברים שאני אוהב לזכור במקרים כאלה-

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

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

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

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

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

פיענוח JSON משורת הפקודה עם jq

21/09/2023

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

פוסט זה זמין גם בתור מדריך וידאו למנויי האתר בקישור: https://www.tocode.co.il/boosters/jq

המשך קריאה

ספריה במקום הבנה?

20/09/2023

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

a = []
for i in [1, 2]:
    a.append(lambda: i)
for f in a:
    print(f())

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

וכן דרך אחת זו הפונקציה מהפרויקט scope_capture.

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

from functools import partial

a = []
for i in [1, 2]:
    a.append(partial(lambda i: i, i))
for f in a:
    print(f())

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

  1. לא צריך להתקין כלום, לא הוספתי תלות חדשה לפרויקט.

  2. הקוד משתמש במבנה יותר גנרי - הפונקציה partial. מתכנתים חדשים שיגיעו לקוד אולי ילמדו להשתמש בה גם בהקשרים אחרים.

נשווה את זה לקוד של הספריה scope_capture:

import inspect
from types import FunctionType

def _make_cell(value):
    fn = (lambda x: lambda: x)(value)
    return fn.__closure__[0]

def capture(f):
    try:
        frame = inspect.currentframe()
        fake_globals = {}
        fake_globals.update(f.__globals__)
        fake_globals.update(frame.f_back.f_locals)
        captured_cells = []
        if f.__closure__:
            for cell in f.__closure__:
                captured_cells.append(_make_cell(cell.cell_contents))
        call_fn = FunctionType(
            code=f.__code__,
            globals=fake_globals,
            name=f.__name__,
            argdefs=f.__defaults__,
            closure=tuple(captured_cells),
        )
        return call_fn
    finally:
        del frame

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

הצילו גיט - אי אפשר לגשת לקובץ

19/09/2023

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

המשך קריאה

סימני אזהרה לדינמיקה רעילה בפרויקט

17/09/2023

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

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

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

המשך קריאה

מעגל היצירה: בעיה, פיתרון, יכולת, בעיה

16/09/2023

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

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

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

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

זה רק קוד

15/09/2023

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

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

כשאנחנו אומרים ש"קוד זה רק קוד" המשמעות היא-

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

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

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

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

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

שני הכללים של Generators בפייתון

14/09/2023

לפייתון לא אכפת איזה Generators נרצה לכתוב, אבל יש שני כללים שכשאנחנו חורגים מהם כדאי לחשוב שנית אם Generator הוא הפיתרון הנכון-

  1. גנרטור לא מחשב את כל הערכים מראש.

  2. גנרטור לא תופס יותר זיכרון ככל שמחשבים יותר פריטים.

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

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

def groupby(sequence, key):
    groups = defaultdict(list)
    for item in sequence:
        groups[key(item)].append(item)

    for key, values in groups.items():
        yield key, values

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

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

import time
import random

def uniq(seq):
    seen = set()
    for i in seq:
        if i not in seen:
            yield i
            seen.add(i)

def random_numbers():
    while True:
        yield random.randint(1, 100)

for i in uniq(random_numbers()):
    time.sleep(2)
    print(i)

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

import time
import random

def random_numbers():
    while True:
        yield random.randint(1, 100)

seen = set()
for i in random_numbers():
    if i not in seen:
        time.sleep(2)
        print(i)
        seen.add(i)

איך להפעיל בקלות פונקציית C מתוך פייתון

13/09/2023

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

קוד? בטח. התוכנית הבאה טוענת את libc ומפעילה את הפונקציה rand מתוכה:

from ctypes import *
from ctypes.util import find_library

libc_location = find_library('c')
libc = cdll.LoadLibrary(libc_location)

print(libc.rand())

פשוט לא? בשביל להעביר פרמטרים לתוכנית C יש לנו 3 סוגי משתנים מובנים ב ctypes שאפשר להעביר כמו שהם:

  1. האוביקט None יהפוך ל NULL ב C

  2. אוביקט Bytes יהפוך ל const chat * ב C

  3. מספר int יהפוך ל int ב C.

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

from ctypes import *
from ctypes.util import find_library

libc_location = find_library('c')
libc = cdll.LoadLibrary(libc_location)

libc.mkdir(b"newdir")

בשביל העברת פרמטרים יותר מתוחכמים אנחנו צריכים להגדיר את טיפוסי הנתונים של הפונקציות ב C. זו אגב נקודת התורפה של ספריית ctypes, שמקבלת מענה רק בספריה אחרת בשם cffi, אבל זה כבר סיפור לפוסט אחר. בינתיים בכל מקרה ובתוך ctypes הדרך להפעיל פונקציות שצריכות פרמטרים יותר מתוחכמים היא להגדיר את טיפוסי הנתונים של אותן פונקציות. לדוגמה הפונקציה pow של libm מקבלת שני double-ים ומחזירה double ולכן בשביל להפעיל אותה אני כותב:

from ctypes import *
from ctypes.util import find_library

libm = cdll.LoadLibrary(find_library('m'))

# Works - use c_double, returns c_double
libm.pow.restype = c_double
libm.pow.argtypes = [c_double, c_double]
print(libm.pow(c_double(2), c_double(3)))

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

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