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

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

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

דרישות קודמות

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

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

  • ידע על סוגי נתונים מובנים בפייתון (רשימות, מילונים, וכדומה).

  • התקנה פעילה של פייתון 3 מומלצת כדי לעסוק בדוגמאות כאן.

תוכן עניינים

  1. מהם השיטות הקסומות?

  2. ייצוג אובייקט

  3. עומס מפענח

  4. שיטות התכנית

  5. גישה לאטריבוטים

  6. מנהלי קשר

  7. אובייקטים ניתנים לקריאה

  8. שיטות קסם מתקדמות

  9. שיקולי ביצועים

  10. שיטות מומלצות

  11. סיכום

  12. הפניות

מהם שיטות הקסם?

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

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

הנה דוגמה פשוטה שממחישה כיצד זה עובד:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2  # זה קורא ל-p1.__add__(p2)
print(p3.x, p3.y)  # פלט: 4 6

בואו נברר מה קורה כאן:

  1. אנו יוצרים מחלקת Point שמייצגת נקודה במרחב דו-ממדי

  2. שיטת ה-__init__ מאתחלת את קואורדינטות ה-x וה-y

  3. שיטת ה-__add__ מגדירה מה קורה כאשר אנו מוסיפים שתי נקודות

  4. כאשר אנו כותבים p1 + p2, פייתון קוראת באופן אוטומטי ל-p1.__add__(p2)

  5. התוצאה היא Point חדש עם קואורדינטות (4, 6)

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

ייצוג אובייקט

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

str מול repr

השיטות __str__ ו- __repr__ משרתות מטרות שונות:

  • __str__: נקראת על ידי הפונקציה str() ועל ידי הפונקציה print(). היא אמורה להחזיר מחרוזת שניתן לקרוא אותה עבור משתמשי הקצה.

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

כאן דוגמה שמציגה את ההבדל:

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def __str__(self):
        return f"{self.celsius}°C"

    def __repr__(self):
        return f"Temperature({self.celsius})"

temp = Temperature(25)
print(str(temp))      # פלט: 25°C
print(repr(temp))     # פלט: טמפרטורה(25)

בדוגמה זו:

  • __str__ מחזיר מחרוזת ידידותית למשתמש שמציגה את הטמפרטורה עם סמל מעלות

  • __repr__ מחזיר מחרוזת שמראה איך ליצור את האובייקט, שימושי לצורך ניפוי שגיאות

ההבדל מתבהר כאשר משתמשים באובייקטים אלו בהקשרים שונים:

  • כאשר אתה מדפיס את הטמפרטורה, אתה רואה את הגרסה הידידותית למשתמש: 25°C

  • כאשר אתה בודק את האובייקט בקונסולת פייתון, אתה רואה את הגרסה המפורטת: טמפרטורה(25)

דוגמה מעשית: מחלקת שגיאה מותאמת אישית

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

class ValidationError(Exception):
    def __init__(self, field, message, value=None):
        self.field = field
        self.message = message
        self.value = value
        super().__init__(self.message)

    def __str__(self):
        if self.value is not None:
            return f"Error in field '{self.field}': {self.message} (got: {repr(self.value)})"
        return f"Error in field '{self.field}': {self.message}"

    def __repr__(self):
        if self.value is not None:
            return f"ValidationError(field='{self.field}', message='{self.message}', value={repr(self.value)})"
        return f"ValidationError(field='{self.field}', message='{self.message}')"

# שימוש
try:
    age = -5
    if age < 0:
        raise ValidationError("age", "Age must be positive", age)
except ValidationError as e:
    print(e)  # פלט: שגיאה בשדה 'גיל': הגיל חייב להיות חיובי (קיבלנו: -5)

מחלקת השגיאות המותאמת זו מספקת מספר יתרונות:

  1. היא כוללת את שם השדה בו התרחשה השגיאה

  2. היא מציגה את הערך הממשי שגרם לשגיאה

  3. היא מספקת הודעות שגיאה שמתאימות למשתמש ופרטיות

  4. היא עוזרת בתהליך האיתור שגיאות על ידי כלול כל המידע הרלוונטי

טעינת מפעילים

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

מפעילים חשבוניים

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

מפעיל שיטת קסם תיאור
+ __add__ חיבור
- __sub__ חיסור
* __mul__ כפל
/ __truediv__ חלוקה
// __floordiv__ חלוקה שלמה
% __mod__ שארית
** __pow__ חזקה

אופרטורים להשוואה

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

אופרטור שיטת קסם תיאור
== __eq__ שווה ל
!= __ne__ לא שווה ל
< __lt__ קטן מ
> __gt__ גדול מ
<= __le__ קטן או שווה ל
>= __ge__ גדול או שווה ל

דוגמה מעשית: מחלקת כסף

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

from functools import total_ordering
from decimal import Decimal

@total_ordering  # מיישם את כל שיטות ההשוואה בהתבסס על __eq__ ו- __lt__
class Money:
    def __init__(self, amount, currency="USD"):
        self.amount = Decimal(str(amount))
        self.currency = currency

    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot add different currencies: {self.currency} and {other.currency}")
        return Money(self.amount + other.amount, self.currency)

    def __sub__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot subtract different currencies: {self.currency} and {other.currency}")
        return Money(self.amount - other.amount, self.currency)

    def __mul__(self, other):
        if isinstance(other, (int, float, Decimal)):
            return Money(self.amount * Decimal(str(other)), self.currency)
        return NotImplemented

    def __truediv__(self, other):
        if isinstance(other, (int, float, Decimal)):
            return Money(self.amount / Decimal(str(other)), self.currency)
        return NotImplemented

    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.currency == other.currency and self.amount == other.amount

    def __lt__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot compare different currencies: {self.currency} and {other.currency}")
        return self.amount < other.amount

    def __str__(self):
        return f"{self.currency} {self.amount:.2f}"

    def __repr__(self):
        return f"Money({repr(float(self.amount))}, {repr(self.currency)})"

בואו נפרק את התכונות המרכזיות של מחלקת Money:

  1. טיפול בדיוק: אנו משתמשים ב- Decimal במקום float כדי להימנע מבעיות דיוק בנקודות צפות בחישובי כסף.

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

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

  4. NotImplemented: כאשר פעולה אינה הגיונית, אנו מחזירים NotImplemented כדי לאפשר לפייתון לנסות את הפעולה ההפוכה.

  5. @total_ordering: הקישוט הזה מיישם אוטומטית את כל שיטות ההשוואה בהתבסס על __eq__ ו-__lt__.

הנה איך להשתמש במחלקת Money:

# אריתמטיקה בסיסית
wallet = Money(100, "USD")
expense = Money(20, "USD")
remaining = wallet - expense
print(remaining)  # פלט: USD 80.00

# עבודה עם מטבעות שונים
salary = Money(5000, "USD")
bonus = Money(1000, "USD")
total = salary + bonus
print(total)  # פלט: USD 6000.00

# חלוקה בסקלר
weekly_pay = salary / 4
print(weekly_pay)  # פלט: USD 1250.00

# השוואות
print(Money(100, "USD") > Money(50, "USD"))  # פלט: True
print(Money(100, "USD") == Money(100, "USD"))  # פלט: True

# טיפול בשגיאות
try:
    Money(100, "USD") + Money(100, "EUR")
except ValueError as e:
    print(e)  # פלט: אי אפשר להוסיף מטבעות שונים: USD ו-EUR

מחלקת Money הזו מדגימה מספר מושגים חשובים:

  1. איך להתמודד עם סוגים שונים של אופרנדים

  2. איך ליישם טיפול שגיאות נכון

  3. איך להשתמש בקישוט @total_ordering

  4. כיצד לשמור דיוק בחישובים פיננסיים

  5. כיצד לספק שיטות מחרוזת וייצוג

שיטות תוצאות

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

פרוטוקול רצציה

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

שיטה תיאור דוגמה לשימוש
__len__ מחזיר את אורך המאגר len(obj)
__getitem__ מאפשר אינדוקציה עם obj[key] obj[0]
__setitem__ מאפשר השמה עם obj[key] = value obj[0] = 42
__delitem__ מאפשר מחיקה עם del obj[key] del obj[0]
__iter__ מחזיר מאיץ עבור המאגר for item in obj:
__contains__ מיישם את אופרטור ה-in 42 in obj

פרוטוקול מיפוי

לפעול דומה למילון, יש לממש את השיטות הבאות:

שיטה תיאור שימוש דוגמה
__getitem__ קבלת ערך לפי מפתח obj["key"]
__setitem__ הגדרת ערך לפי מפתח obj["key"] = value
__delitem__ מחיקת זוג מפתח-ערך del obj["key"]
__len__ קבלת מספר זוגות מפתח-ערך len(obj)
__iter__ עבר על המפתחות for key in obj:
__contains__ בדיקה האם המפתח קיים "key" in obj

דוגמה מעשית: מטמון מותאם אישית

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

import time
from collections import OrderedDict

class ExpiringCache:
    def __init__(self, max_age_seconds=60):
        self.max_age = max_age_seconds
        self._cache = OrderedDict()  # {מפתח: (ערך, זמן סימון)}

    def __getitem__(self, key):
        if key not in self._cache:
            raise KeyError(key)

        value, timestamp = self._cache[key]
        if time.time() - timestamp > self.max_age:
            del self._cache[key]
            raise KeyError(f"Key '{key}' has expired")

        return value

    def __setitem__(self, key, value):
        self._cache[key] = (value, time.time())
        self._cache.move_to_end(key)  # העבר לסוף כדי לשמור על סדר ההכנסה

    def __delitem__(self, key):
        del self._cache[key]

    def __len__(self):
        self._clean_expired()  # מנקה פריטים שפג תוקפם לפני הדיווח על אורך
        return len(self._cache)

    def __iter__(self):
        self._clean_expired()  # מנקה פריטים שפג תוקפם לפני האיטרציה
        for key in self._cache:
            yield key

    def __contains__(self, key):
        if key not in self._cache:
            return False

        _, timestamp = self._cache[key]
        if time.time() - timestamp > self.max_age:
            del self._cache[key]
            return False

        return True

    def _clean_expired(self):
        """Remove all expired entries from the cache."""
        now = time.time()
        expired_keys = [
            key for key, (_, timestamp) in self._cache.items()
            if now - timestamp > self.max_age
        ]
        for key in expired_keys:
            del self._cache[key]

בואו נבדוק איך זה עובד:

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

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

  3. שיטות מיכל: המחלקה מיישמת את כל השיטות הנחוצות כדי להתנהג דומה למילון:

    • __getitem__: מביא ערכים ובודק תפוגה

    • __setitem__: מאחסן ערכים עם ציון זמן נוכחי

    • __delitem__: מסיר רשומות

    • __len__: מחזיר מספר של רשומות לא פגות תוקפן

    • __iter__: מנטר על מפתחות לא פגות תוקפן

    • __contains__: בודק האם מפתח קיים

כך ניתן להשתמש במטמון:

צור מטמון עם פגיעות של 2 שניות
cache = ExpiringCache(max_age_seconds=2)

# אחסן ערכים מסוימים
cache["name"] = "Vivek"
cache["age"] = 30

# גישה לערכים
print("name" in cache)  # פלט: True
print(cache["name"])    # פלט: Vivek
print(len(cache))       # פלט: 2

# המתן לפגיעה
print("Waiting for expiration...")
time.sleep(3)

# בדוק ערכים שפגו
print("name" in cache)  # פלט: False
try:
    print(cache["name"])
except KeyError as e:
    print(f"KeyError: {e}")  # פלט: KeyError: 'name'

print(len(cache))  # פלט: 0

מימוש המטמון הזה מספק מספר יתרונות:

  1. פגיעה אוטומטית של רשומות ישנות

  2. ממשק דומה למילון לשימוש קל

  3. יעילות זיכרון על ידי הסרת רשומות שפגו

  4. פעולות בטוחות לסלילה (בהנחה על גישה חד-תהליך)

  5. שמירה על סדר ההכנסה של הרשומות

גישת מאפיינים

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

getattr ו־getattribute

פייתון מספקת שני שיטות לשליטה בגישה למאפיינים:

  1. __getattr__: נקראת רק כאשר חיפוש מאפיין נכשל (כלומר, כאשר המאפיין לא קיים)

  2. __getattribute__: נקראת עבור כל גישה למאפיין, גם עבור מאפיינים שקיימים

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

כאן דוגמה פשוטה המציגה את ההבדל:

class AttributeDemo:
    def __init__(self):
        self.name = "Vivek"

    def __getattr__(self, name):
        print(f"__getattr__ called for {name}")
        return f"Default value for {name}"

    def __getattribute__(self, name):
        print(f"__getattribute__ called for {name}")
        return super().__getattribute__(name)

demo = AttributeDemo()
print(demo.name)      # פלט: __getattribute__ נקראת עבור name
                      #        Vivek
print(demo.age)       # פלט: __getattribute__ נקראת עבור age
                      #        __getattr__ נקראת עבור age
                      #        ערך ברירת מחדל עבור age

setattr ו־delattr

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

  1. __setattr__: נקרא כאשר מאפיין מתעדכן

  2. __delattr__: נקרא כאשר מאפיין נמחק

שיטות אלו מאפשרות לך ליישם אימות, רישום או התנהגות מותאמת כאשר מאפיינים משתנים.

דוגמה מעשית: תכונות רישום אוטומטי

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

import logging

# הקמת רישום
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

class LoggedObject:
    def __init__(self, **kwargs):
        self._data = {}
        # אתחול מאפיינים מבלי להפעיל את __setattr__
        for key, value in kwargs.items():
            self._data[key] = value

    def __getattr__(self, name):
        if name in self._data:
            logging.debug(f"Accessing attribute {name}: {self._data[name]}")
            return self._data[name]
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

    def __setattr__(self, name, value):
        if name == "_data":
            # אפשר לקבוע את מאפיין _data ישירות
            super().__setattr__(name, value)
        else:
            old_value = self._data.get(name, "<undefined>")
            self._data[name] = value
            logging.info(f"Changed {name}: {old_value} -> {value}")

    def __delattr__(self, name):
        if name in self._data:
            old_value = self._data[name]
            del self._data[name]
            logging.info(f"Deleted {name} (was: {old_value})")
        else:
            raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

בואו נפרק איך המחלקה הזו פועלת:

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

  2. גישה למאפיינים:

    • __getattr__: מחזיר ערכים מ-_data ומרשום הודעות דיבוג

    • __setattr__: מאחסן ערכים ב-_data ומרשום שינויים

    • __delattr__: מסיר ערכים מ-_data ומרשום מחיקות

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

כיצד להשתמש במחלקה:

# צור אובייקט עם ערכים התחלתיים
user = LoggedObject(name="Vivek", email="[email protected]")

# שנה תכונות
user.name = "Vivek"  # יומנים: שם שונה: Vivek -> Vivek
user.age = 30         # יומנים: גיל שונה: <לא מוגדר> -> 30

# גישה לתכונות
print(user.name)      # פלט: Vivek

# מחק תכונות
del user.email        # יומנים: דוא"ל נמחק (היה: [email protected])

# נסה לגשת לתכונה שנמחקה
try:
    print(user.email)
except AttributeError as e:
    print(f"AttributeError: {e}")  # פלט: AttributeError: לאובייקט 'LoggedObject' אין תכונה 'email'

יישום זה מספק מספר יתרונות:

  1. יומנים אוטומטיים של כל שינויי התכונות

  2. יומני רמת דיבאג לגישה לתכונות

  3. הודעות שגיאה ברורות עבור תכונות חסרות

  4. ניטור קל של שינויים במצב האובייקט

  5. מועיל לדיבאג ולביקורת

מנגד הקשר

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

הכניסה והיציאה

כדי ליצור מנהל קרנות, עליך ליישם שני מתודות קסם:

  1. __enter__: נקרא כאשר נכנסים לתוך בלוק ה-with. עליו להחזיר את המשאב שיש לנהלו.

  2. __exit__: נקרא כאשר יוצאים מתוך בלוק ה-with, גם אם אירעה חריגה. עליו לטפל בניקוי.

המתודה __exit__ מקבלת שלושה ארגומנטים:

  • exc_type: סוג החריגה (אם קיימת)

  • exc_val: מופע החריגה (אם קיים)

  • exc_tb: ציוץ החריגה (אם קיים)

דוגמה מעשית: מנהל חיבור למסד נתונים

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

import sqlite3
import logging

# הגדרת רישום
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

class DatabaseConnection:
    def __init__(self, db_path):
        self.db_path = db_path
        self.connection = None
        self.cursor = None

    def __enter__(self):
        logging.info(f"Connecting to database: {self.db_path}")
        self.connection = sqlite3.connect(self.db_path)
        self.cursor = self.connection.cursor()
        return self.cursor

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            logging.error(f"An error occurred: {exc_val}")
            self.connection.rollback()
            logging.info("Transaction rolled back")
        else:
            self.connection.commit()
            logging.info("Transaction committed")

        if self.cursor:
            self.cursor.close()
        if self.connection:
            self.connection.close()

        logging.info("Database connection closed")

        # החזרת ערך שקר להעברת חריגות, אמת להשתיקן
        return False

בואו נפרק איך מנהל ההקשר הזה עובד:

  1. אתחול:

    • המחלקה מקבלת את נתיב מסד הנתונים

    • היא מאתחלת את החיבור והסדר כאפס

  2. שיטת הכניסה:

    • יוצרת חיבור למסד נתונים

    • יוצרת סדר

    • מחזירה את הסדר לשימוש בבלוק with

  3. שיטת יציאה:

    • מנהל טיפול בעסקאות (אישור/ביטול)

    • סוגר עקומית וחיבור

    • מספר פעולות בלוג

    • מחזיר False כדי להעביר חריגות

כך משתמשים במנהל ההקשר:

# יצירת מסד נתונים למבחן בזיכרון
try:
    # עסקה מוצלחת
    with DatabaseConnection(":memory:") as cursor:
        # יצירת טבלה
        cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT
            )
        """)

        # הכנסת נתונים
        cursor.execute(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            ("Vivek", "[email protected]")
        )

        # שאילתת נתונים
        cursor.execute("SELECT * FROM users")
        print(cursor.fetchall())  # פלט: [(1, 'ויבק', '[email protected]')]

    # הדגמת ביטול עסקה במקרה של שגיאה
    with DatabaseConnection(":memory:") as cursor:
        cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT
            )
        """)
        cursor.execute(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            ("Wewake", "[email protected]")
        )
        # זה יגרום לשגיאה - הטבלה 'nonexistent' לא קיימת
        cursor.execute("SELECT * FROM nonexistent")
except sqlite3.OperationalError as e:
    print(f"Caught exception: {e}")

מנהל ההקשר הזה מספק מספר יתרונות:

  1. משאבים ניהלים אוטומטית (לדוגמא: חיבורים תמיד נסגרים).

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

  3. חריגות נתפסות ומטופלות בצורה נאותה

  4. כל הפעולות מתעדות לצורך איתור באגים

  5. ההוראה with עושה את הקוד ברור ומדויק

פונקציות קריאות

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

קריאה

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

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, x):
        return x * self.factor

# יצירת מופעים שנראים כמו פונקציות
double = Multiplier(2)
triple = Multiplier(3)

print(double(5))  # פלט: 10
print(triple(5))  # פלט: 15

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

דוגמה מעשית: קישוט Memoization

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

import time
import functools

class Memoize:
    def __init__(self, func):
        self.func = func
        self.cache = {}
        # שימור מטאדאטה של פונקציה (שם, תיאור, וכו')
        functools.update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        # יצירת מפתח מהארגומנטים
        # לפשטות, אנו מניחים שכל הארגומנטים ניתנים להאשה
        key = str(args) + str(sorted(kwargs.items()))

        if key not in self.cache:
            self.cache[key] = self.func(*args, **kwargs)

        return self.cache[key]

# שימוש
@Memoize
def fibonacci(n):
    """Calculate the nth Fibonacci number recursively."""
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# מדידת זמן ביצוע
def time_execution(func, *args, **kwargs):
    start = time.time()
    result = func(*args, **kwargs)
    end = time.time()
    print(f"{func.__name__}({args}, {kwargs}) took {end - start:.6f} seconds")
    return result

# ללא ממוּזציה, זה היה איטי מאוד
print("Calculating fibonacci(35)...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")

# הקריאה השנייה היא מיידית בזכות ממוּזציה
print("\nCalculating fibonacci(35) again...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")

בואו ננתח כיצד דקורטור הממוּזציה הזה עובד:

  1. אתחול:

    • מקבל פונקציה כארגומנט

    • יוצר מילון מטמון לאחסון תוצאות

    • שומר את המטאדאטה של הפונקציה בעזרת functools.update_wrapper

  2. קריאת שיטה:

    • יוצר מפתח ייחודי מארגומנטי הפונקציה

    • בודק אם התוצאה נמצאת במטמון

    • אם לא, מחשב את התוצאה ושומר אותה

    • מחזיר את התוצאה מהמטמון

  3. שימוש:

    • מיושם כדקורטור לכל פונקציה

    • מטמן אוטומטית תוצאות לקריאות חוזרות

    • שומר על מטא-נתונים והתנהגות הפונקציה

היתרונות של המימוש כוללים:

  1. ביצועים טובים יותר, משום שממנע חישובים מיותרים

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

  3. זה גמיש וניתן לשימוש עם כל פונקציה

  4. זה יעיל מבחינת זיכרון ומאפשר אחסון תוצאות לשימוש חוזר

  5. זה שומר על תיעוד הפונקציה

שיטות קסם מתקדמות

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

new ליצירת אובייקט

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

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

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, name=None):
        # פונקציה זו תיקרא בכל פעם שנקרא ל-Singleton()
        if name is not None:
            self.name = name

# שימוש
s1 = Singleton("Vivek")
s2 = Singleton("Wewake")
print(s1 is s2)  # פלט: True
print(s1.name)   # פלט: Wewake (האיניציאליזציה השנייה דרסה את הראשונה)

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

  1. משתנה מחלקה: _instance שומר על המופע היחיד של המחלקה

  2. שיטת new:

    • בודקת האם קיים מופע

    • יוצרת אחד אם לא קיים

    • מחזירה את המופע הקיים אם קיים

  3. שיטת init:

    • מופעלת בכל פעם שהבנאי משתמש

    • מעדכנת את המאפיינים של המופע

חריגים לאופטימיזצית זיכרון

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

הנה השוואה בין מחלקות רגילות ומוגנות:

import sys

class RegularPerson:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

class SlottedPerson:
    __slots__ = ['name', 'age', 'email']

    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

# השוואת שימוש בזיכרון
regular_people = [RegularPerson("Vivek" + str(i), 30, "[email protected]") for i in range(1000)]
slotted_people = [SlottedPerson("Vivek" + str(i), 30, "[email protected]") for i in range(1000)]

print(f"Regular person size: {sys.getsizeof(regular_people[0])} bytes")  # פלט: גודל אדם רגיל: 48 בתים
print(f"Slotted person size: {sys.getsizeof(slotted_people[0])} bytes")  # פלט: גודל אדם בעל חריצים: 56 בתים
print(f"Memory saved per instance: {sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])} bytes")  # פלט: חיסכון בזיכרון לכל מופע: -8 בתים
print(f"Total memory saved for 1000 instances: {(sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])) * 1000 / 1024:.2f} KB")  # פלט: סכום החיסכון בזיכרון עבור 1000 מופעים: -7.81 קילובייט

הרצת הקוד הזה תוביל לתוצאה מעניינת:

Regular person size: 48 bytes
Slotted person size: 56 bytes
Memory saved per instance: -8 bytes
Total memory saved for 1000 instances: -7.81 KB

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

אז מה קורה פה? החיסכון האמיתי בזיכרון מ- __slots__ מגיע מתוך:

  1. השלבת המילונים: עצמים רגילים בפייתון שומרים את התכונות שלהם במילון (__dict__), שיש לו עלויות. פונקציית sys.getsizeof() לא מתייחסת לגודל של המילון הזה.

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

  3. קיימות: היתרון האמיתי מופיע כאשר:

    • יש לך הרבה מופעים (אלפיים או מיליונים)

    • האובייקטים שלך מכילים הרבה מאפיינים

    • אתה מוסיף מאפיינים באופן דינמי

בואו נראה השוואה מלאה יותר:

# מדידת זיכרון מדויקת יותר
import sys

def get_size(obj):
    """Get a better estimate of the object's size in bytes."""
    size = sys.getsizeof(obj)
    if hasattr(obj, '__dict__'):
        size += sys.getsizeof(obj.__dict__)
        # הוספת גודל תוכן המילון
        size += sum(sys.getsizeof(v) for v in obj.__dict__.values())
    return size

class RegularPerson:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

class SlottedPerson:
    __slots__ = ['name', 'age', 'email']

    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

regular = RegularPerson("Vivek", 30, "[email protected]")
slotted = SlottedPerson("Vivek", 30, "[email protected]")

print(f"Complete Regular person size: {get_size(regular)} bytes")  # פלט: גודל כולל של אדם רגיל: 610 בתים
print(f"Complete Slotted person size: {get_size(slotted)} bytes")  # פלט: גודל כולל של אדם עם סלוטים: 56 בתים

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

נקודות מרכזיות על __slots__:

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

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

  3. שיקולי מורשת: שימוש ב־__slots__ עם מורשת מחייב תכנון זהיר

  4. מקרי שימוש: מתאים ביותר למחלקות עם הרבה מופעים ומאפיינים קבועים

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

חסר לערכי מחרוזת ברירת מחדל

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

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

class AutoKeyDict(dict):
    def __missing__(self, key):
        self[key] = []
        return self[key]

# שימוש
groups = AutoKeyDict()
groups["team1"].append("Vivek")
groups["team1"].append("Wewake")
groups["team2"].append("Vibha")

print(groups)  # פלט: {'קבוצה1': ['ויבק', 'וויק'], 'קבוצה2': ['ויבה']}

מימוש זה מספק מספר יתרונות:

  1. אין צורך לבדוק האם מפתח קיים, מה שנוח יותר.

  2. האתחול האוטומטי יוצר ערכי ברירת מחדל כפי שנדרש.

  3. מפחית את הקוד המייתר באתחול של מילון.

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

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

התחשבויות ביצועים

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

השפעת שיטות המג'יק על הביצועים

לשיטות המג'יק השונות יש השפעות שונות על הביצועים:

שיטות גישה למאפיינים:

  • __getattr__, __getattribute__, __setattr__, ו־__delattr__ נקראות בתדירות

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

שיטות מיכון

  • __getitem__, __setitem__, ו-__len__ נקראים לעיתים קרובות בלולאות

  • יישומים לא יעילים יכולים להאט את הקונטיינר שלך הרבה יותר מסוגים מובנים

העמסת מפעילים:

  • מפעילי חשבון והשוואה משמשים לעיתים קרובות

  • יישומים מורכבים יכולים להאט פעולות פשוטות באופן מפתיע

בואו נמדוד את השפעת הביצועים של __getattr__ מול גישה ישירה למאפיין:

import time

class DirectAccess:
    def __init__(self):
        self.value = 42

class GetAttrAccess:
    def __init__(self):
        self._value = 42

    def __getattr__(self, name):
        if name == "value":
            return self._value
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

# מדוד ביצועים
direct = DirectAccess()
getattr_obj = GetAttrAccess()

def benchmark(obj, iterations=1000000):
    start = time.time()
    for _ in range(iterations):
        x = obj.value
    end = time.time()
    return end - start

direct_time = benchmark(direct)
getattr_time = benchmark(getattr_obj)

print(f"Direct access: {direct_time:.6f} seconds")
print(f"__getattr__ access: {getattr_time:.6f} seconds")
print(f"__getattr__ is {getattr_time / direct_time:.2f}x slower")

הרצת בדיקה זו מראה הבדלי ביצועים משמעותיים:

Direct access: 0.027714 seconds
__getattr__ access: 0.060646 seconds
__getattr__ is 2.19x slower

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

אסטרטגיות אופטימיזציה

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

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

  2. שמרו ערכים מחושבים : אתם יכולים לאחסן תוצאות של פעולות יקרות ולעדכן את המטמון רק כשיש צורך. השתמשו ב@property עבור אטריבוטים מחושבים.

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

מנהגים הכי טובים

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

1. היו עקביים

כאשר מממשים מתודות קסומות קשורות, שמרו על עקביות בהתנהגות:

from functools import total_ordering

@total_ordering
class ConsistentNumber:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        if not isinstance(other, ConsistentNumber):
            return NotImplemented
        return self.value == other.value

    def __lt__(self, other):
        if not isinstance(other, ConsistentNumber):
            return NotImplemented
        return self.value < other.value

2. החזירו NotImplemented

כאשר פעולה אינה הגיונית, החזירו NotImplemented כדי לאפשר לפייתון לנסות את הפעולה ההפוכה:

class Money:
    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        # ... שאר המימוש 

3. שמרו על זה פשוט

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

# טוב: פשוט וצפוי
class SimpleContainer:
    def __init__(self):
        self.items = []

    def __getitem__(self, index):
        return self.items[index]

# רע: מורכב ועשוי להיות מבלבל
class ComplexContainer:
    def __init__(self):
        self.items = []
        self.access_count = 0

    def __getitem__(self, index):
        self.access_count += 1
        if self.access_count > 100:
            raise RuntimeError("Too many accesses")
        return self.items[index]

4. תעדו את ההתנהגות

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

class CustomDict(dict):
    def __missing__(self, key):
        """
        Called when a key is not found in the dictionary.
        Creates a new list for the key and returns it.
        This allows for automatic list creation when accessing
        non-existent keys.
        """
        self[key] = []
        return self[key]

5. שקלו את הביצועים

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

class OptimizedContainer:
    __slots__ = ['items']  # השתמשו ב- __slots__ לביצועים טובים יותר

    def __init__(self):
        self.items = []

    def __getitem__(self, index):
        return self.items[index]  # גישה ישירה מהירה יותר

6. ניהול מקרים קצה

תמיד שקלו מקרים קצה וטפלו בהם בצורה מתאימה:

class SafeContainer:
    def __getitem__(self, key):
        if not isinstance(key, (int, slice)):
            raise TypeError("Index must be integer or slice")
        if key < 0:
            raise ValueError("Index cannot be negative")
        # ... שאר היישום

סיכום

שיטות הקסם של פייתון מספקות דרך עוצמתית לגרום למחלקות שלכם להתנהג כמו סוגים מובנים, מה שמאפשר קוד אינטואיטיבי יותר וביטוי יותר. throughout this guide, we've explored how these methods work and how to use them effectively.

נקודות מפתח

  1. ייצוג אובייקט:

    • השתמשו ב- __str__ לפלט ידידותי למשתמש

    • השתמשו ב- __repr__ עבור דיבוג ופיתוח

  2. עומס המפעילים:

    • יישום מפעילי חשבון והשוואה

    • החזרת NotImplemented לפעולות שאינן נתמכות

    • השתמש ב־@total_ordering להשוואות עקביות

  3. התנהגות המיכל:

    • יישום פרוטוקולי רצף ומיפוי

    • שיקול ביצוע לפעולות הנפוצות

    • טיפול במקרים קצה באופן הולם

  4. ניהול משאבים:

    • השתמש במנהלי הקשר לטיפול נכון במשאבים

    • יישם __enter__ ו-__exit__ לניקוי

    • טפל בשגיאות ב-__exit__

  5. אופטימיזציה של ביצועים:

    • השתמש ב-__slots__ ליעילות זיכרון

    • אחסן ערכים מחושבים כאשר יש צורך

    • מזער קריאות מתודיות בקוד בשימוש תדיר

מתי להשתמש בשיטות קסם

שיטות קסם שימושיות במיוחד כשאתה צריך:

  1. צור מבני נתונים מותאמים אישית

  2. מממש סוגי דומיין מיוחדים

  3. נהל משאבים בצורה תקינה

  4. הוסף התנהגות מיוחדת למחלקות שלך

  5. שפר את הקוד שלך כך שיהיה פייתוני יותר

מתי להימנע משימוש בשיטות קסם

הימנע משימוש בשיטות קסם כאשר:

  1. גישה פשוטה למאפיינים מספיקה

  2. ההתנהגות תהיה מבלבלת או בלתי צפויה

  3. ביצועים מכריעים ושימוש בשיטות קסם יוסיף עומס

  4. המימוש יהיה מורכב מדי

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

הפניות וקריאה נוספת

מסמכי הפייתון הרשמיים

  1. Python מודל נתונים – תיעוד רשמי – מדריך מקיף למודל הנתונים ולשיטות הקסם של פייתון.

  2. functools.total_ordering – תיעוד עבור הקישוט total_ordering שממלא אוטומטית שיטות השוואה חסרות.

  3. שמות שיטות מיוחדות בפייתון – מדריך רשמי לזהויות של שיטות מיוחדות בפייתון.

  4. מחלקות המקור המופשט של אוספים – למידה על מחלקות המקור המופשט של תופעות שמגדירות את הממשקים שאפשר לממש במחלקות האוסף שלך.

משאבי קהילה

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

קריאה נוספת

אם נהניתם מהמאמר, ייתכן שתמצאו את המאמרים הבאים הקשורים לפייתון בבלוג האישי שלי:

  1. ניסויים מעשיים לאופטימיזציות שאילתות ORM של Django – למידה כיצד לאופטימז פניות ORM של Django שלך עם דוגמאות מעשיות וניסויים.

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