פוסט אורח: Data Objects בפייתון - מ tuple ועד dataclass

פוסט אורח מאת ליאור אלבז

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

מבוא

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

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

print_shape(color='red', category='rectangle', size=10)                                                                              

אבחר להעביר לה אובייקט באופן הבא:

print_shape(my_shape)                                                                                                                              

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


הרבה פעמים כשאנחנו רוצים להעביר מידע בקוד, נשתמש במבנים כמו list/tuple/dict. לפעמים זה בסדר גמור, לtuple יש הרבה יתרונות, הוא native data type, מהיר, חסכוני בזכרון והסינטקס שלו אינטואיטיבי מאוד (מי לא התלהב מ tuple unpacking כשרק התחיל לכתוב בפייתון?).

tuple

בואו נסתכל על 2 דוגמאות פשוטות של העברה וניתוח מידע באמצעות tuple:

my_shape = 'red', 'rectangle', 10
if my_shape[2] > 5:
        print('That's a big shape')                                                                                                                    

או בגרסה היותר פופולרית

my_shape = 'red', 'rectangle', 10
_, _, size = my_shape
if size > 5:
        print("That's a big shape!")                                                                                                                  

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

dict

בואו נסתכל על דוגמה יותר טובה:

my_shape = {'category': 'rectangle', 'colour': 'red', 'size': 10}
if my_shape['color'] == 'red':
    print('I also like red!')                                                                                                                           

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

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

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

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

namedtuple

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

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


אך namedtuple בא גם עם המגבלות של tuple מבחינת גמישות.

class

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

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

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

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

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

class Shape:
    def __init__(self, color: str, category: str, size: float):
        self.color = color
        self.category = catagory
        self.size = size

    def __repr__(self):
        return f'{color={self.color}, category={self.category}, size={self.size}'                                                 

בעצם, כל פעם שארצה לכתוב מחלקה שתייצג אובייקט מידע, אצטרך לממש עבורה את פונקציית ה-init ופונקציות אחרת שאצטרך כמו str, repr, hash, compare וכו׳..

dataclass for the rescue

למזלנו החל מפייתון 3.7 חלק גדול מהעבודה הזו נחסך לנו.

dataclasses הוא בעצם module מהספריה הסטנדרטית של פייתון (החל מגרסה 3.7).

כדי להבין איך עובדים איתם, נסתכל על המימוש של shape:

from dataclasses import dataclass                                                                                                              

@dataclass
class Shape:
color : str
category: str
size: float

וזהו, אלגנטי, קצר.

בעצם המודל הזה כבר מממש את כל מה שעשיתי למעלה, ויותר, את ה init,repr,hash,compare וכל הדברים הטובים האלה לבד.

dataclasses מביא לנו את הטוב מכל העולמות, זוכרים את tuple unpacking שדיברתי עליו אי שם בתחילת הפוסט?
אז ב dataclasses יש את המתודה: unpacked.

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

ומה אם אני רוצה שהמידע שאני יוצר יהיה immutable?
אין בעיה, בדקורטור של @dataclass נוסיף (frozen=True).

ובנוסף להתנהגות המוכרת והאהובה של מחלקות עבור ערך דיפולטיבי, נוכל לקבל ערך דיפולטיבי שבא ממתודה כלשהי בעזרת הארגומנט default_factory בתוך dataclasses fields

@dataclass(frozen=True)
class Shape:
size: float
      hidden_score: typing.Optional[int] = field(repr=False)
color: str = 'black'
category: str = field(default_factory=get_random_shape())                                                                

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

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

סיכום

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


Happy coding


תגובות

הוסף רשומת תגובה

פוסטים פופולריים מהבלוג הזה

תכנות מונחה עצמים | Dependency Inversion Principle

מהם קבצי DLL ואיך להשתמש בהם?

מה ההבדל בין אוטומציה לפיתוח רגיל