שלום אורח התחבר

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

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

מעדיפים לקרוא מהטלגרם? בקרו אותנו ב:@tocodeil

או הזינו את כתובת המייל וקבלו את הפוסט היומי בכל בוקר אליכם לתיבה:

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

בגלל שכל אחד שילם מחיר קצת שונה היה נוח לחלק את המחירים לקבוצות עם pd.cut וכך הגעתי לקוד הבא:

import pandas as pd

url = 'https://raw.githubusercontent.com/guipsamora/pandas_exercises/master/07_Visualization/Titanic_Desaster/train.csv'
titanic = pd.read_csv(url)

s=pd.cut(titanic['Fare'], bins=[0, 10, 20, 50, 100, 1000], right=True)
data = s.value_counts()

print(data)

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

(0, 10]        321
(20, 50]       216
(10, 20]       179
(50, 100]      107
(100, 1000]     53
Name: Fare, dtype: int64

והנה מאפייני הטיטאניק:

In: titanic['Fare'].describe()
Out:

count    891.000000
mean      32.204208
std       49.693429
min        0.000000
25%        7.910400
50%       14.454200
75%       31.000000
max      512.329200
Name: Fare, dtype: float64

קל לראות שבחלוקה לסלים בשביל לצייר את הגרף איבדנו 15 נתוני כרטיסים.

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

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

המשך קריאה...

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

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

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

המשך קריאה...

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

אבל כשעוברים ממשחקים לעולם האמיתי אנחנו מוצאים קבצי CSV פחות נחמדים: החל מקבצים ישנים שמקודדים ב Extended ASCII או במקרה העוד יותר גרוע קבצים שמערבבים תווים בכל מיני קידודים.

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

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

>> pd.read_csv('053cea08-09bc-40ec-8f7a-156f0677aff3.csv', '|')

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xee in position 0: invalid continuation byte

עד לפה לא מבהיל מדי כי תמיד אפשר להעביר Encoding לפנדס. אבל הפעם גם זה לא עבד:

>> pd.read_csv('053cea08-09bc-40ec-8f7a-156f0677aff3.csv', encoding='iso8859-8')

UnicodeDecodeError: 'charmap' codec can't decode byte 0xd2 in position 127430: character maps to <undefined>

מה עושים? תשמחו לשמוע שיש פיתרון יחסית פשוט: נפתח את הקובץ עם הקידוד שאנחנו רוצים ונבקש מ open שתחליף תווים שלא תואמים לקידוד בסימני שאלה. את ה File Handle נעביר לפנדס שישמח לקבל את המידע אחרי פיענוח. הקוד כולו נראה כך:

>> fd = open('053cea08-09bc-40ec-8f7a-156f0677aff3.csv', encoding='iso8859-8', errors='replace')
>> df = pd.read_csv(fd, '|')

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

df.groupby('tozeret_nm').size().sort_values(ascending=False)

tozeret_nm
מזדה יפן         299175
יונדאי קוריאה    228002
קיה קוריאה       205592
טויוטה יפן       189340
סקודה צ'כיה      173473

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

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

https://www.gov.il/he/Departments/DynamicCollectors/cities-ramzor

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

https://www.gov.il/he/api/DataGovProxy/GetDGResults

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

המשך קריאה...

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

def first_letter(text):
    return text[0]

text = "I can see a mountain",
print(first_letter(text))

הצלחתם לנחש מה הקוד מדפיס? נריץ ונבדוק אם צדקנו:

$ python3 ouch.py 
I can see a mountain

את כל הטקסט??? פייתון...

הבעיה היא כמובן הפסיק בסוף שורת ההשמה:

text = "I can see a mountain",

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

וזאת אולי הסיבה המרכזית שכדאי להוסיף Type Hints לקוד שלכם. הכתיב הבא:

def first_letter(text: str):
    return text[0]

text = "I can see a mountain",
print(first_letter(text))

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

$ python3 -m mypy ouch.py
ouch.py:5: error: Argument 1 to "first_letter" has incompatible type "Tuple[str]"; expected "str"
Found 1 error in 1 file (checked 1 source file)

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

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

המשך קריאה...

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

>>> 2 == 2 == 2

והוא ענה לי True.

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

x = 30
if 20 < x < 50:
    print("I \U00002764 Python")

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

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

הכיוון הראשון שעבד נראה כך:

input_range = range(145852, 616942)
total = 0

for candidate in input_range:
    flag_all_up = True
    flag_same = False
    cand_str = str(candidate)
    for i in range(5):
        if cand_str[i] == cand_str[i+1]:
            flag_same = True
        elif cand_str[i] > cand_str[i+1]:
            flag_all_up = False
            break

    if flag_all_up and flag_same:
        total += 1

print(total)

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

def all_up(candidate):
    cand_str = str(candidate)
    for i in range(5):
        if cand_str[i] > cand_str[i+1]:
            return False

    return True

def has_two_adjacent_digits(candidate):
    cand_str = str(candidate)
    for i in range(5):
        if cand_str[i] == cand_str[i+1]:
            return True
    return False

total = sum(
        map(lambda candidate: all_up(candidate) and has_two_adjacent_digits(candidate),
            input_range))
print(total)

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

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

class TwoAdjacentDigitsPredicate:
    def __init__(self):
        self.result = False

    def process(self, a, b):
        if a == b:
            self.result = True

class AllUpPredicate:
    def __init__(self):
        self.result = True

    def process(self, a, b):
        if a > b:
            self.result = False

def apply_predicates(val, *predicates):
    val = str(val)
    for i in range(5):
        for p in predicates:
            p.process(val[i], val[i+1])
    return all([p.result for p in predicates])

total = filter(lambda candidate: apply_predicates(
            candidate,
            TwoAdjacentDigitsPredicate(), AllUpPredicate()), input_range)

print(sum(1 if x else 0 for x in total))

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

יש לכם כבר ניחוש מה זמני הריצה לכל גירסא? הנה זה בא:

# First version - single function
python3 aoc.py  0.81s user 0.02s system 85% cpu 0.965 total
# Second version - multiple functions
python3 aoc.py  0.54s user 0.01s system 97% cpu 0.568 total
# Third version - Object Oriented
python3 aoc.py  2.52s user 0.04s system 83% cpu 3.072 total

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

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

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

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

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

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

הנה התוכנית ב-4 שורות.

from gtts import gTTS

google_voice = gTTS(text="Hello world", lang='en')
google_voice.save('sound.mp3')

לפני שנעבור על הקוד שורה אחרי שורה קחו שתי דקות להקשיב לו. הכנסו לקישור: https://repl.it/@ynonp/AjarFatalCallbacks#main.py

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

בתוך הרפליט תוכלו ללחוץ על כפתור Run כדי להריץ את התוכנית, ואז בצד שמאל תראו שנוצר קובץ חדש בשם sound.mp3. לחצו עליו ותשמעו את גוגל אומר Hello world. ואפשר גם לשחק עם הקוד: שנו את הטקסט בתוכנית מ Hello world לכל משפט אחר, הריצו מחדש ותוכלו לשמוע את גוגל אומר את המשפט החדש.

המשך קריאה...