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

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

ציונים

16/04/2023

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

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

היום, לפחות בתעשיית ההייטק, התשתית הזאת כמעט לא קיימת.

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

כשבתי ספר ממשיכים לדרג תלמידים לפי ציונים הם מזיקים פעמיים:

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

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

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

קפיצה גדולה מדי

15/04/2023

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

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

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

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

מבוא זריז לספריית spaCy

14/04/2023

ספייסי היא ספריית nlp בפייתון שיודעת לעשות הרבה דברים ש nltk עושה אבל הממשק שלה קצת יותר נוח ויש אומרים שחלק מהדברים היא עושה מהר או מדויק יותר. יש להם תמיכה במעל 72 שפות, ופייפליינים ל 24 שפות (מה שאומר שעם 24 שפות אפשר לעשות ממש הרבה דברים). אני מאוד אהבתי בספייסי שהכל עבד מהקופסה וכשדברים לא עבדו מספיק טוב אפשר היה לבחור פייפליין שונה כדי לשפר את הדיוק.

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

תחילה התקנה - pip עובד כאן רגיל לגמרי:

$ pip install spacy

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

$ python -m spacy download en_core_web_trf

ובשביל ספרדית אפשר להוריד את:

$ python -m spacy download es_dep_news_trf

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

>>> import spacy
>>> en = spacy.load("en_core_web_trf")

את הפונקציה en שנוצרה אני יכול להפעיל על משפט או קטע טקסט באנגלית כדי לקבל אוביקט spacy.tokens.doc.Doc שמתאים לו. אחרי זה אפשר להשתמש באותו אוביקט כדי לקבל מידע על המילים:

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

  2. מכל טוקן אפשר לקחת את המאפיין pos_ כדי לקבל מה תפקידו במשפט.

  3. המאפיין lemma של טוקן מחזיר את הצורה הבסיסית של הדבר (ללא הטיות).

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

>>> text = 'Inflectional morphology is the process by which a root form of a word is modified by adding prefixes or suffixes that specify its grammatical function but do not change its part-of-speech. We say that a lemma (root form) is inflected (modified/combined) with one or more morphological features to create a surface form.'

>>> doc = en(text)
>>> for token in doc[:10]:
>>>     print(token.text, token.pos_, token.lemma_)

Inflectional ADJ inflectional
morphology NOUN morphology
is AUX be
the DET the
process NOUN process
by ADP by
which PRON which
a DET a
root NOUN root
form NOUN form

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

import spacy
from spacy.matcher import Matcher

nlp = spacy.load("en_core_web_sm")
matcher = Matcher(nlp.vocab)
# Add match ID "HelloWorld" with no callback and one pattern
pattern = [{'POS': 'ADJ', 'OP': '?'},
           {'LEMMA': 'match', 'POS': 'NOUN'},
           {'LEMMA': 'be'}]
matcher.add("match", [pattern])

doc = nlp("""
A match is a tool for starting a fire. Typically, modern matches are made of small wooden sticks or stiff paper. One end is coated with a material that can be ignited by frictional heat generated by striking the match against a suitable surface. Wooden matches are packaged in matchboxes, and paper matches are partially cut into rows and stapled into matchbooks.
""")

matches = matcher(doc)
for match_id, start, end in matches:
    string_id = nlp.vocab.strings[match_id]  # Get string representation
    span = doc[start:end]  # The matched span
    print(match_id, string_id, start, end, span.text)

שימו לב למשתנה pattern: הקוד מחפש רצף של מילים שיכול להתחיל ב Adjective, אחריו המילה match בתור שם עצם (יחיד או רבים) ואחריה הפועל be. פלט התוכנית הוא:

16065818573247886523 match 2 4 match is
16065818573247886523 match 13 16 modern matches are
16065818573247886523 match 14 16 matches are
16065818573247886523 match 49 52 Wooden matches are
16065818573247886523 match 50 52 matches are
16065818573247886523 match 58 60 matches are

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

במחשבה שלישית

13/04/2023

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

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

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

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

$ git -c user.name ynon -c user.email ynon@tocode.co.il commmit -m '...'

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

היום למדתי: פרופילים ב docker compose

12/04/2023

מה קורה אם אתם צריכים להפעיל גם משימה חד פעמית בפרויקט שמשתמש ב docker compose? טוב דרך אחת היא להתחבר לקונטיינר שכבר מריץ את היישום ולהפעיל את המשימה מתוכו. לדוגמה נניח שיש לי את התוכן הבא ב docker-compose.yml:

version: "3.9"
services:
  backend:
    image: backend

  db:
    image: mysql

ויש לי סקריפט בשם myapp migrate שאני יכול להפעיל רק מתוך האימג' של backend, אז אני יכול להעלות את הסרביסים וכשצריך להפעיל את הסקריפט:

$ docker compose up -d
$ docker compose run backend bash -c "myapp migrate"

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

הייתי מעדיף לכתוב סרביס עבור הסקריפט ב docker-compose.yml שיכלול גם את הפקודה ויאפשר להריץ את הסקריפט עם הפרמטרים שאני צריך והאימג' הרלוונטי:

version: "3.9"
services:
  backend:
    image: backend

  db:
    image: mysql

  migrate:
    image: backend
    command: myapp migrate

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

מסתבר שיש פיתרון פשוט ומובנה ב docker compose שנקרא פרופילים: אנחנו מציינים לכל סרביס לאיזה פרופיל הוא שייך. סרביסים שלא שייכים לאף פרופיל יעלו כשאני כותב docker compose up, וסרביסים אחרים יופעלו כשאני מפעיל את הפרופיל או כשאני מפעיל אותם בצורה יזומה. במקרה של הסקריפט שלי זה אומר לעבור ל docker-compose.yml הבא:

version: "3.9"
services:
  backend:
    image: backend

  db:
    image: mysql

  migrate:
    image: backend
    command: myapp migrate
    profiles: ["tools"]

עכשיו אני יכול להפעיל docker compose up כדי להפעיל את המערכת הרגילה, או docker compose run migrate כדי להפעיל את סקריפט המיגרציה.

ניהול מוטיבציה

11/04/2023

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

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

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

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

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

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

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

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

יבוא מעגלי ו Redux Tolkit

10/04/2023

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

המשך קריאה

טיפ פייתון: שימו לב ל b

09/04/2023

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

>>> import base64
>>> base64.b64decode("bmluamE=")
b'ninja'
>>> base64.b64decode("bW9ua2V5")
b'monkey'
>>> base64.b64decode("bG92ZQ==")
b'love'
>>>

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

import base64
import sys

enc = sys.argv[1]

users = ["monkey", "love", "ninja"]

if base64.b64decode(enc) in users:
    print("welcome")

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

אופציה 1:

import base64
import sys

enc = sys.argv[1]

users = [b"monkey", b"love", b"ninja"]

if base64.b64decode(enc) in users:
    print("welcome")

אופציה 2:

import base64
import sys

enc = sys.argv[1]

users = ["monkey", "love", "ninja"]

if base64.b64decode(enc).decode("utf-8") in users:
    print("welcome")

למה לא נשלח מייל הבוקר (או: מה בא אחרי I)

08/04/2023

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

hebcal | head -10
1/3/2023 Asara B'Tevet
1/23/2023 Rosh Chodesh Sh'vat
2/6/2023 Tu B'Shvat
2/18/2023 Shabbat Shekalim
2/21/2023 Rosh Chodesh Adar
2/22/2023 Rosh Chodesh Adar
3/4/2023 Shabbat Zachor
3/6/2023 Ta'anit Esther
3/7/2023 Purim
3/8/2023 Shushan Purim

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

evenings_ok=(
"Pesach I"
"Pesach VII"
"Shavuot I"
"Tish'a B'Av"
"Rosh Hashana II"
"Yom Kippur"
"Sukkot I"
"Shmini Atzeret"
)

for send_in_evening in "${evenings_ok[@]}"
do
        # echo "^[0-9/]+ ${send_in_evening}"
        if hebcal $(date +"%m %d %Y") | egrep -v "CH''M" | egrep "^[0-9/]+ ${send_in_evening}" >& /dev/null
        then
                if (( $(date +%"_H") < 20 ))
                then
                        exit 1
                fi
        fi
done

אז מה שבור כאן? הפסח כמובן. המחרוזת Pesach I שמייצגת את חג הפסח באמת מתאימה לטקסט Pesach I שיודפס באותו יום, אבל היא מתאימה גם ל Pesach II:

hebcal | grep 'Pesach I'
4/6/2023 Pesach I
4/7/2023 Pesach II
4/8/2023 Pesach III (CH''M)
4/9/2023 Pesach IV (CH''M)

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

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

$ hebcal | grep 'Pesach I$'
4/6/2023 Pesach I

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

evenings_ok=(
"Pesach I$"
"Pesach VII"
"Shavuot I$"
"Tish'a B'Av"
"Rosh Hashana II"
"Yom Kippur"
"Sukkot I"
"Shmini Atzeret"
)

no_emails=(
"Rosh Hashana"
)

no_emails_erev=(
"Erev Pesach"
"Erev Shavuot"
"Erev Rosh Hashana"
"Erev Sukkot"
"Erev Yom Kippur"
)


for send_in_evening in "${evenings_ok[@]}"
do
        # echo "^[0-9/]+ ${send_in_evening}"
        if hebcal $(date +"%m %d %Y") | egrep -v "CH''M" | egrep "^[0-9/]+ ${send_in_evening}" >& /dev/null
        then
                if (( $(date +%"_H") < 20 ))
                then
                        exit 1
                fi
        fi
done

for holiday in "${no_emails[@]}"
do
        if hebcal $(date +"%m %d %Y") | egrep "^[0-9/]+ ${holiday}" >& /dev/null
        then
                exit 1
        fi
done



for erev_holiday in "${no_emails_erev[@]}"
do
        if hebcal $(date +"%m %d %Y") | egrep "^[0-9/]+ ${erev_holiday}" >& /dev/null && [$(date +"_H") > 19]
        then
                exit 1
        fi
done

טיפ פייתון: פיתוח עם docker compose

07/04/2023

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

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

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

בשביל לפתח על המחשב שלנו קוד פייתון עם docker-compose נצטרך להתמודד עם האתגרים הבאים:

  1. נרצה שהקוד בתוך הקונטיינר יהיה זהה לקוד בספריית הפיתוח.

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

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

  4. נעדיף לא לבנות אימג' בשביל הפיתוח כדי לחסוך פעולות build.

המשך קריאה