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

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

טיפ נאמפיי: החזרת אינדקסים מפונקציה

04/01/2021

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

import numpy as np

arr = np.arange(1, 11) * np.arange(1, 11).reshape(-1, 1)

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

In [6]: arr[0:3,0:3]
Out[6]: 
array([[1, 2, 3],
       [2, 4, 6],
       [3, 6, 9]])

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

def get_index():
    # THIS DOES NOT WORK
    return 0:3,0:3

print(arr[get_index()])

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

In [11]: def get_index():
    ...:     return (slice(0, 3), slice(0, 3))

In [12]: arr[get_index()]
Out[12]: 
array([[1, 2, 3],
       [2, 4, 6],
       [3, 6, 9]])

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

איך לברוח מלולאה כפולה ב Python

31/12/2020

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

for i in values:
    for j in values:
        for k in values:
            if i != j and j != k and i + j + k == 2020:
                print(f"Found! {i}, {j}, {k}")
                break

זה עובד, אבל... כך נראית התוצאה:

Found! 289, 480, 1251
Found! 289, 1251, 480
Found! 480, 289, 1251
Found! 480, 1251, 289
Found! 1251, 289, 480
Found! 1251, 480, 289

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

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

def triplets(values):
    for i in values:
        for j in values:
            for k in values:
                if i != j and j != k:
                    yield(i, j, k)


for i, j, k in triplets(values):
    if i + j + k == 2020:
        print(f"Found! {i}, {j}, {k}")
        break

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

הפונקציה itertools.product היא דרך גנרית לבנות כזה מנגנון ובצורה מקוצרת כך שהקוד הבא עובד מעולה:

import itertools
for i, j, k in itertools.product(values, values, values):
    if i != j != k and i + j + k == 2020:
        print(f"Found! {i}, {j}, {k}")
        break

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

for i, j, k in itertools.combinations(values, 3):
    if i + j + k == 2020:
        print(f"Found! {i}, {j}, {k}")
        break

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

קונדה-מה?

28/12/2020

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

המשך קריאה

טיפים לעבודה יעילה על מספר סביבות פייתון במקביל

25/12/2020

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

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

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

המשך קריאה

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

18/11/2020

בפוסט שעלה לא מזמן ב Python Tips הציע יהונתן להגדיר slots כדי לבנות אוביקטים קטנים יותר. תוצאת לוואי מעניינת של הפיצ'ר היא שהוא מונע מאנשים להוסיף בטעות Attribute לאוביקט.

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

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

  1. שדה id מסוג int.
  2. שדה name מסוג str.
  3. שדה friends מסוג מערך של Person.

הקוד באמצעות Type Hints יראה כך:

from typing import List

class Person:
    id: int
    name: str
    friends: List['Person']

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

p = Person()
x = Person()
y = Person()

# Works great
p.id = 0
p.name = 'test'
p.friends = [x, y]

# Fails: 10 is not a Person
p.friends.append(10)

# Fails: foo is not a valid attribute
p.foo = 'bar'

הפעלה של mypy על הקוד נותנת:

a.py:22: error: Argument 1 to "append" of "list" has incompatible type "int"; expected "Person"
a.py:25: error: "Person" has no attribute "foo"

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

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

  2. פונקציית __repr__ אוטומטית שמחזירה ייצוג ידידותי של האוביקט

  3. פונקציית __eq__ שבודקת שיוויון לפי כל אחד מהשדות

  4. פונקציה בשם dataclasses.asdict שלוקחת כל Data Class ומחזירה dict עם כל המאפיינים שלו.

סך הכל אחרי המרה לשימוש ב dataclass המחלקה שלי נראית כך:

from typing import List
from dataclasses import dataclass, field
import dataclasses


@dataclass
class Person:
    id: int
    name: str
    friends: List['Person'] = field(default_factory=list)


p = Person(0, 'foo')
x = Person(1, 'bar')
y = Person(2, 'buz')

print(dataclasses.asdict(p))

בזכות פונקציית __init__ שנוצרה אוטומטית אני יכול ליצור אוביקטים של Person ולהעביר אליהם את כל הפרמטרים עבורם לא מוגדר ערך ברירת מחדל (ויש Type Hint שמוודא את זה), ואני יכול לקרוא ל asdict כדי לקבל את id, name ו friends בתוך dict.

למידע נוסף על dataclasses שווה לקרוא את דף התיעוד בקישור: https://docs.python.org/3/library/dataclasses.html

חידת פנדס: לאן נעלמו הערכים?

25/09/2020

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

בגלל שכל אחד שילם מחיר קצת שונה היה נוח לחלק את המחירים לקבוצות עם 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 נתוני כרטיסים? באיזו שורה הבעיה? ומה השינוי הקטן שאפשר לבצע כדי לתקן אותה?

תרגיל פייתון פשוט

24/09/2020

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

המשך קריאה

פיתוח מערכת פלאגינים פשוטה ב Python

22/09/2020

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

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

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

המשך קריאה

טיפ פנדס: איך להתעלם מטעויות Encoding בקובץ

11/09/2020

הפקודה 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