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

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

מה כותבים קודם - ממשק משתמש או לוגיקה?

16/10/2023

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

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

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

היתרונות של כתיבת ממשק המשתמש קודם הם לכן:

  1. אני לא כותב לוגיקה מיותרת אלא רק את המנגנונים שהמוצר והמשתמשים שלו צריכים.

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

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

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

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

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

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

  4. הרבה יותר קל לשנות ממשק משתמש כשיש שכבת מידע יציבה מאשר לשנות שכבת מידע כשיש ממשק משתמש יציב.

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

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

בואו נסדר טקסט בטבלה עם הפקודה column

15/10/2023

הפקודה column נועדה כדי לקבל טקסט ארוך ולשבור אותו לעמודות כדי שיהיה קל יותר לקרוא אותו בלי לגלול, כמו טורים בדף של עיתון. אפשר לראות דוגמה קלה לשימוש בה כשנעביר אליה תוכן של קובץ. כך נראה הקובץ /etc/shells אצלי:

$ cat /etc/shells

# List of acceptable shells for chpass(1).
# Ftpd will not allow users to connect who are not using
# one of these shells.

/bin/bash
/bin/csh
/bin/dash
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh
/usr/local/bin/pwsh

וכך זה נראה עם column:

$ cat /etc/shells | column

# List of acceptable shells for chpass(1).                      /bin/csh                                                        /bin/tcsh
# Ftpd will not allow users to connect who are not using        /bin/dash                                                       /bin/zsh
# one of these shells.                                          /bin/ksh                                                        /usr/local/bin/pwsh
/bin/bash                                                       /bin/sh

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

#!/bin/bash

end=${1:-10}

for ((i=1; i <= $end; i++))
do
  for ((j=1; j <= $end; j++))
  do
    printf "%d " $((i * j))
  done
  echo
done

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

1 2 3 4 5 6 7 8 9 10
2 4 6 8 10 12 14 16 18 20
3 6 9 12 15 18 21 24 27 30
4 8 12 16 20 24 28 32 36 40
5 10 15 20 25 30 35 40 45 50
6 12 18 24 30 36 42 48 54 60
7 14 21 28 35 42 49 56 63 70
8 16 24 32 40 48 56 64 72 80
9 18 27 36 45 54 63 72 81 90
10 20 30 40 50 60 70 80 90 100

התוספת של column -t כבר עושה את הקסם:

$ bash table.bash | column -t

1   2   3   4   5   6   7   8   9   10
2   4   6   8   10  12  14  16  18  20
3   6   9   12  15  18  21  24  27  30
4   8   12  16  20  24  28  32  36  40
5   10  15  20  25  30  35  40  45  50
6   12  18  24  30  36  42  48  54  60
7   14  21  28  35  42  49  56  63  70
8   16  24  32  40  48  56  64  72  80
9   18  27  36  45  54  63  72  81  90
10  20  30  40  50  60  70  80  90  100

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

ubuntu@primary:~/Home/tmp/blog$ bash table.bash |column -t -R0
 1   2   3   4   5   6   7   8   9   10
 2   4   6   8  10  12  14  16  18   20
 3   6   9  12  15  18  21  24  27   30
 4   8  12  16  20  24  28  32  36   40
 5  10  15  20  25  30  35  40  45   50
 6  12  18  24  30  36  42  48  54   60
 7  14  21  28  35  42  49  56  63   70
 8  16  24  32  40  48  56  64  72   80
 9  18  27  36  45  54  63  72  81   90
10  20  30  40  50  60  70  80  90  100

ואם התוצאה היתה מיידית?

14/10/2023

ואם אחרי הסיגריה הראשונה כבר היית חוטף סרטן ריאות?

ואם אחרי הגיוס הגרוע הראשון החברה היתה מתמוטטת?

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

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

  1. אנחנו מנסים לתקן את הבעיה באמצעות טלאי.

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

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

קורס? ייעוץ? מה בעצם אני צריך?

13/10/2023

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

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

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

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

תזכורת מ curl - מי ביקש ממך?

12/10/2023

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

יש לו גם משתנה בשם socks_proxy.proxytype שיכול לקבל את הערך CURLPROXY_SOCKS5 בשביל לפענח לבד את כתובת ה IP אליה צריך להתחבר, או את הערך CURLPROXY_SOCKS5_HOSTNAME בשביל להעביר את שם השרת ישירות לפרוקסי שהוא כבר ישבור את הראש. אבל במצב שצריך להעביר את שם השרת יש בעיה - שרתי socks תומכים רק בשמות שרת באורך עד 255 תווים.

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

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

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

הקוד הפגיע בגדול הוא:

  bool socks5_resolve_local =
    (conn->socks_proxy.proxytype == CURLPROXY_SOCKS5) ? TRUE : FALSE;
  const size_t hostname_len = strlen(sx->hostname);
  ssize_t len = 0;
  const unsigned char auth = data->set.socks5auth;
  bool allow_gssapi = FALSE;
  struct Curl_dns_entry *dns = NULL;

  DEBUGASSERT(auth & (CURLAUTH_BASIC | CURLAUTH_GSSAPI));
  switch(sx->state) {
  case CONNECT_SOCKS_INIT:
    if(conn->bits.httpproxy)
      infof(data, "SOCKS5: connecting to HTTP proxy %s port %d",
            sx->hostname, sx->remote_port);

    /* RFC1928 chapter 5 specifies max 255 chars for domain name in packet */
    if(!socks5_resolve_local && hostname_len > 255) {
      infof(data, "SOCKS5: server resolving disabled for hostnames of "
            "length > 255 [actual len=%zu]", hostname_len);
      socks5_resolve_local = TRUE;
    }

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

המסקנה של דניאל נכונה גם לגבינו ולכל קוד שאנחנו כותבים. הוא כותב:

curl should not switch mode from remote resolve to local resolve due to too long host name. It should rather return an error and starting in curl 8.4.0, it does.

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

פשוט כי ככה ראיתי בדוגמה

11/10/2023

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

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

כל שפת שאילתות לגרפים מאפשרת לציין את כיוון הקשת או להשמיט אותו. אז בסייפר יהיה לי:

MATCH (tom:Person {name:'Tom Hanks'})-[rel:DIRECTED]-(movie:Movie)
RETURN tom.name, tom.born, movie.title, movie.released

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

MATCH (:Person {name: 'Tom Hanks'})-[:DIRECTED]->(movie:Movie)
RETURN movie.title

שתופסת רק את הסרטים כשכיוון הקשת הוא מהשחקן לסרט. בגרמלין אם יש לי בגרף צומת מסוג person בשם marko וממנו יוצאת קשת בשם created לצומת שמייצג פרויקט שמרקו יצר אז השאילתה:

g.V().has('person','name','marko').out('created')

תחזיר את הפרויקט בהנחה שכיוון הקשת הוא מהיוצר אל הפרויקט, והשאילתה:

g.V().has('person','name','marko').both('created')

תחזיר את הפרויקט בלי להתחייב על כיוון הקשת.

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

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

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

מה בעצם בדקת שם?

10/10/2023

נתבונן בקוד הבדיקה הבא מתוך leantime:

    #[Depends('Tests\Acceptance\LoginCest:loginSuccessfully')]
    public function createAUser(AcceptanceTester $I)
    {
        $I->wantTo('Create a user');
        $I->amOnPage('/users/showAll');
        $I->click('Add User');
        $I->waitForElement('#firstname', 120);
        $I->fillField('#firstname', 'John');
        $I->fillField('#lastname', 'Doe');
        $I->selectOption('#role', 'Read Only');
        $I->selectOption('#client', 'Not assigned to a client');
        $I->fillField('#user', 'john@doe.com');
        $I->fillField('#phone', '1234567890');
        $I->fillField('#jobTitle', 'Testing');
        $I->fillField('#jobLevel', 'Testing');
        $I->fillField('#department', 'Testing');
        $I->click('Invite User');
        echo $I->grabPageSource();
        $I->waitForElement('.growl', 120);
        $I->wait(2);
        $I

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

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

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

  2. הבדיקה מוודאת שיש ב HTML אלמנטים של טופס עם המזהים שמופיעים בקוד הבדיקה.

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

  4. הבדיקה מוודאת שהטופס מוגש כשניגשים ל URL שמופיע בקוד הבדיקה.

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

  1. אין בדיקה שמשתמש אכן נוצר בבסיס הנתונים.

  2. אין בדיקה שפרטי המשתמש שנוצר קשורים לפרטים שהוזנו בטופס.

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

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

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

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

אני יודע שעוד אתחרט על זה

09/10/2023

כשאני כותב פונקציה של 200 שורות.

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

כשאני כותב קוד שאין לי איך לבדוק אותו.

כשאני שומר את המפתח בתוך הקוד.

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

כשאני מעתיק קטע קוד מ Chat GPT או Stack Overflow, בלי להבין אותו עד הסוף.

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

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

כך נראית בעיית הבנה

07/10/2023

נתבונן בתוכנית הבאה בפייתון שמחשבת את ה sh256 של הקובץ /etc/passwd:

import hashlib
from pathlib import Path

data = Path("/etc/passwd").read_text().encode("utf8")
print(hashlib.sha256(data).hexdigest())

וכן העובדה שהקוד עובד רק מחמירה את המצב.

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

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

import hashlib
from pathlib import Path

data = Path("/etc/passwd").read_text()
print(hashlib.sha256(data).hexdigest())

מקבלים את הודעת השגיאה:

TypeError: Strings must be encoded before hashing

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

import hashlib
from pathlib import Path

data = Path("/etc/passwd").read_bytes()
print(hashlib.sha256(data).hexdigest())

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

import hashlib
from pathlib import Path

file_path = "/etc/passwd"

try:
    with open(file_path, 'rb') as file:
        file_content = file.read()
        hash_value = hashlib.sha256(file_content).hexdigest()
        print(f"SHA-256 Hash of '{file_path}': {hash_value}")
except FileNotFoundError:
    print(f"Error: File '{file_path}' not found.")
except PermissionError:
    print(f"Error: Permission denied while trying to read '{file_path}'.")
except Exception as e:
    print(f"Error: {e}")

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