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

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

פיענוח 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 ולתרגם אותן אוטומטית לפייתון, וכך לחסוך לעצמנו את ההגדרה הכפולה.

טייפסקריפט ללא טייפסקריפט: קריאת JSDoc

12/09/2023

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

המשך קריאה