חיפוש באוביקט מקונן בפייתון

29/05/2017

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

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

1. הקוד

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

movie_data = {
  "movies": {
    "Spaceballs": {
      "imdb_stars": 7.1,
      "rating": "PG",
      "length": 96,
      "director": "Mel Brooks",
    },
    "Robin_Hood_Men_in_Tights": {
      "imdb_stars": 6.7,
      "rating": "PG-13",
      "length": 104,
      "director": "Mel Brooks",
    }
  }
}

movie_box = Box(movie_data)
print(movie_box.movies.Robin_Hood_Men_in_Tights.imdb_stars)

הפונקציה __getattr__ נקראת פעמיים: קודם על האוביקט moviebox עם השם `RobinHoodMeninTightsולאחר מכן על האוביקט שחזר מהקריאה הקודמת עם השםimdbstars`. לכן, הטריק במימוש הוא פשוט להחזיר אוביקט Box חדש בכל קריאה:

class Box:
    def __init__(self, d):
        self._d = d

    def __getattr__(self, name):
        if type(self._d[name]) == dict:
            return Box(self._d[name])
        else:
            return self._d[name]

כך אפשר לשלוף שדות מהאוביקט בכל עומק שתרצו.

2. בעיות במימוש

הקוד הבא מדפיס תוצאה מפתיעה:


movie_data = {
  "_d": 7,
  "movies": {
    "Spaceballs": {
      "imdb_stars": 7.1,
      "rating": "PG",
      "length": 96,
      "director": "Mel Brooks",
    },
    "Robin_Hood_Men_in_Tights": {
      "imdb_stars": 6.7,
      "rating": "PG-13",
      "length": 104,
      "director": "Mel Brooks",
    }
  }
}

movie_box = Box(movie_data)
print(movie_box._d)

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

שינוי קטן במחלקה Box מאפשר לתקן גם את זה:

class Box:
    def __init__(self, d):
        self._d = d

    def __getattribute__(self, name):
        _d = object.__getattribute__(self, '_d')
        val = _d[name]
        if type(val) == dict:
            return Box(val)
        else:
            return _d[name]

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