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

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

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

04/05/2023

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

ניקח לדוגמה את הרשימות:

l1 = ["one", "two", ""]
l2 = ["one", "", "three"]
l3 = ["one", "two", "three"]

עכשיו נשאל - באיזה רשימה כל התאים הם באורך 1 לפחות?

הפונקציה all נותנת דרך מאוד קלה לפתור את הבעיה, בתנאי שאנחנו יודעים לשלב אותה עם הרכבת רשימות: בשלב ראשון נהפוך את כל התאים ברשימה לערך בוליאני שאומר האם התא מתאים לתנאי או לא, ובשלב שני נפעיל את all שתבדוק שכל התאים ברשימה הם True. הקוד יהיה:

def check_list(l: [str]):
    return all((len(i) > 0 for i in l))

שווה לשים לב:

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

  2. אפשר ורצוי לקרוא עוד על all ועל החברה שלה any בדף התיעוד על פונקציות מובנות בפייתון.

מתי למזג ל main?

03/05/2023

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

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

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

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

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

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

מכירים גישות נוספות? איך עובדים אצלכם בצוות? ואיזה גישה אתם מעדיפים?

החזרת מספר בטווח ב TypeScript

02/05/2023

נתבונן בפונקציה הבאה בטייפסקריפט:

function createRandomNumber(): number {
    return Math.floor(Math.random() * 10);
}

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

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

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

type Range10 = 0|1|2|3|4|5|6|7|8|9;
function createRandomNumber() {
    return Math.floor(Math.random() * 10) as Range10;
}

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

הפיתרון הגנרי משתמש ברקורסיה ונראה כך:

export type Range<N extends number,R extends number[]=[]> = 
    R['length'] extends N ?  R[number] : Range<N, [...R, R['length']]>;


function createRandomNumber() {
    return Math.floor(Math.random() * 10) as Range<10>;
}

וההסבר-

  1. הרקורסיה בונה את המערך R, כך שכל איטרציה היא מוסיפה ל R מספר נוסף.

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

  3. בסוף הרקורסיה מחזירים איחוד של כל המספרים השמורים ב R.

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

איך לבלבל את החברים עם קובץ usercustomize.py

01/05/2023

לא רבים יודעים אבל מספר קבצים מסתוריים במערכת הקבצים של משתמשי פייתון. אחד מהם נקרא usercustomize.py. אני לא בטוח מה היתה המטרה המקורית של הפיצ'ר, אבל המצב היום הוא שאם תשימו קובץ כזה בתוך תיקיית site-packages על המחשב שלכם, אז כל פעם שתפעילו סקריפט פייתון המפרש יטען קודם כל את הקובץ usercustomize.py ורק אחר כך יעבור להפעיל את הסקריפט.

(וכן זה עובד אפילו עם ה REPL9).

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

python -c "import site; print(site.getsitepackages())"

אצלי היא הדפיסה:

['/Users/ynonp/.pyenv/versions/3.11.1/lib/python3.11/site-packages']

אני יוצר קובץ חדש בתוך התיקיה בשם usercustomize.py, ובתוכו כותב את התוכן הבא:

print("I will NOT go away. I do NOT wish to go!")

בואו ננסה את זה. אני מפעיל REPL ומקבל את הפלט:

I will NOT go away. I do NOT wish to go!
Python 3.11.1 (main, Apr  6 2023, 09:09:27) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

אני מפעיל שרת HTTP ומקבל:

$ python -m http.server 8000

I will NOT go away. I do NOT wish to go!
Serving HTTP on :: port 8000 (http://[::]:8000/) ...

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

אז למה זה טוב? (חוץ מלבלבל את החברים). הנה כמה רעיונות:

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

  2. אפשר להוסיף עוד ספריות ל PYTHONPATH כדי לטעון מהן מודולים.

  3. אפשר לתעד כל הפעלה של פייתון או למנוע הפעלות מסוימות (לדוגמה אפשר להוסיף sys.exit בתוך הקובץ אם לא רוצים לאפשר הפעלה מסוימת).

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

יש לכם עוד רעיונות טובים לשימוש ב usercustomize.py? נתקלתם ב Use Cases מעניינים איתו? ספרו לי בתגובות.

החיים באי וודאות

30/04/2023

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

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

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

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

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

אבל היכולת לחיות עם אי וודאות היא הכרחית אם החלום שלנו הוא לפתור בעיות מעניינות (כאלה ש Chat GPT לא יודע לפתור).

הנה כמה רעיונות איך לאמן את שריר החיים עם אי הוודאות, בהתחלה על דברים לא חשובים ולאט לאט גם על דברים יותר חשובים-

  1. נסו לראות סידרת טלוויזיה באנגלית בלי כתוביות (או אם האנגלית שלכם טובה בשפה אחרת שאתם לא 100% בה). המטרה היא ללמוד ליהנות גם בלי להבין את כל הדיאלוג.

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

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

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

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

השוואה סובייקטיבית לגמרי בין neo4j ל datomic

29/04/2023

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

המשך קריאה

סיבה ותוצאה

28/04/2023

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

ורציתי לשאול אותה - למה את עושה את זה לעצמך? את הרי כבר כל כך ספורטיבית. בשביל מה את צריכה לצאת שוב לרוץ? למה לסבול ככה?

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

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

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

איפה להשקיע את זמן הלימוד שלי?

27/04/2023

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

שני הצירים מחלקים את העולם ל-4 ריבועים:

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

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

  3. בצד שמאל למעלה אני מוצא ספריות ופריימוורקים עשירים שפותרים את כל הבעיות של העולם אבל קשה מאוד ללמוד אותם (אני מסתכל עליכם ריילס וקוברנטיס).

  4. ובצד שמאל למטה אני מוצא ספריות ופריימוורקים מסובכים שמנסים לפתור את כל הבעיות של העולם אבל כושלים (היוש אנגולר1).

עכשיו בחזרה לשאלה מהכותרת, איפה להשקיע את זמן הלימוד שלי?

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

חיפוש טוב יותר

26/04/2023

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

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

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

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

איך לא לייצר סיסמה אקראית

25/04/2023

ביקשתי מ Chat GPT לכתוב פונקציית פייתון שמחזירה סיסמה אקראית מתוך אורך ורשימה של תווים בסיסמה. זאת היתה התוצאה:

import random

def generate_password(length, symbols):
    password = ''
    for i in range(length):
        password += random.choice(symbols)
    return password

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

import string
import random

def generate_password(length):
    chars = string.ascii_letters + string.digits + string.punctuation
    while True:
        password = ''.join(random.choice(chars) for _ in range(length))
        if (any(c.islower() for c in password)
            and any(c.isupper() for c in password)
            and any(c.isdigit() for c in password)
            and any(c in string.punctuation for c in password)):
            return password

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

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

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

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

  4. קוד נכון מאפשר כלים אוטומטיים - קיימים כלי ניתוח אוטומטיים היכולים לזהות חולשות בקוד. כשאני מקפיד להשתמש בכלים מאובטחים בכל המערכת, יהיה לי יותר קל לשלב את אותם כלים. כשהקוד שלי מלא בקוד לא מאובטח (גם אם בהקשרים לא קריטיים) אותם כלים פשוט יזרקו יותר מדי False Positives ויהיה קשה למצוא את הבעיות האמיתיות בקוד.

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

import os

def make_random_password(length=12, symbols='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@$^_+&'):
    password = []
    for i in map(lambda x: int(len(symbols)*x/255.0), os.urandom(length)):
        password.append(symbols[i])
    return ''.join(password)