האם אי פעם תהיתם איך פייתון גורם לאובייקטים לעבוד עם אופרטורים כמו +
או -
? או איך הוא יודע איך להציג אובייקטים כשאתם מדפיסים אותם? התשובה טמונה בשיטות הקסומות של פייתון, הידועות גם כשיטות דנדר (double under).
שיטות קסומות הן שיטות מיוחדות המאפשרות לכם להגדיר כיצד האובייקטים שלכם מתנהגים בתגובה לפעולות שונות ולפונקציות מובנות. הן מה שעושה את תכנות המונחה-אובייקטים של פייתון כל כך חזק ואינטואיטיבי.
במדריך הזה, תלמדו כיצד להשתמש בשיטות קסומות כדי ליצור קוד יותר אלגנטי וחזק. תראו דוגמאות מעשיות המראות כיצד השיטות הללו פועלות בתרחישים בעולם האמיתי.
דרישות קודמות
-
הבנה בסיסית של תחביר פייתון ומושגי תכנות מונחה-אובייקטים.
-
היכרות עם מחלקות, אובייקטים, וירושה.
-
ידע על סוגי נתונים מובנים בפייתון (רשימות, מילונים, וכדומה).
-
התקנה פעילה של פייתון 3 מומלצת כדי לעסוק בדוגמאות כאן.
תוכן עניינים
מהם שיטות הקסם?
שיטות הקסם בפייתון הן שיטות מיוחדות שמתחילות ומסתיימות בקו תחתון כפול (__
). כאשר אתה משתמש בפעולות או פונקציות מסוימות על האובייקטים שלך, פייתון קוראת באופן אוטומטי לשיטות אלו.
לדוגמה, כאשר אתה משתמש באופרטור +
על שני אובייקטים, פייתון מחפשת את שיטת __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
בואו נברר מה קורה כאן:
-
אנו יוצרים מחלקת
Point
שמייצגת נקודה במרחב דו-ממדי -
שיטת ה-
__init__
מאתחלת את קואורדינטות ה-x וה-y -
שיטת ה-
__add__
מגדירה מה קורה כאשר אנו מוסיפים שתי נקודות -
כאשר אנו כותבים
p1 + p2
, פייתון קוראת באופן אוטומטי ל-p1.__add__(p2)
-
התוצאה היא
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)
מחלקת השגיאות המותאמת זו מספקת מספר יתרונות:
-
היא כוללת את שם השדה בו התרחשה השגיאה
-
היא מציגה את הערך הממשי שגרם לשגיאה
-
היא מספקת הודעות שגיאה שמתאימות למשתמש ופרטיות
-
היא עוזרת בתהליך האיתור שגיאות על ידי כלול כל המידע הרלוונטי
טעינת מפעילים
טעינת המפעילים היא אחת מתכונות החכמה ביותר של שפת פייתון. היא מאפשרת לך להגדיר איך האובייקטים שלך מתנהגים כאשר משתמשים במפעילים כמו +
, -
, *
, ו־==
. זה משפר את האינטואיציה והקריאות של הקוד שלך.
מפעילים חשבוניים
פייתון מספקת שיטות קסם לכל הפעולות החשבוניות הבסיסיות. הנה טבלה המציינת איזו שיטה מתאימה לאיזה מפעיל:
מפעיל | שיטת קסם | תיאור |
+ |
__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
:
-
טיפול בדיוק: אנו משתמשים ב-
Decimal
במקוםfloat
כדי להימנע מבעיות דיוק בנקודות צפות בחישובי כסף. -
בטיחות מטבע: המחלקה מונעת פעולות בין מטבעות שונים כדי להימנע משגיאות.
-
בדיקת סוג: כל שיטה בודקת אם האופרנד השני הוא מהסוג הנכון באמצעות
isinstance()
. -
NotImplemented: כאשר פעולה אינה הגיונית, אנו מחזירים
NotImplemented
כדי לאפשר לפייתון לנסות את הפעולה ההפוכה. -
@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
הזו מדגימה מספר מושגים חשובים:
-
איך להתמודד עם סוגים שונים של אופרנדים
-
איך ליישם טיפול שגיאות נכון
-
איך להשתמש בקישוט
@total_ordering
-
כיצד לשמור דיוק בחישובים פיננסיים
-
כיצד לספק שיטות מחרוזת וייצוג
שיטות תוצאות
שיטות תוצאות מאפשרות לך להפוך את האובייקטים שלך להתנהג כמו מערכות מובנות כמו רשימות, מילונים או סטים. זה מאוד שימושי כאשר נדרשת התנהגות מותאמת אישית לאחסון ולקבלת נתונים.
פרוטוקול רצציה
כדי להפוך את האובייקט שלך להתנהג כרצצה (כמו רשימה או טאפל), עליך ליישם את השיטות האלה:
שיטה | תיאור | דוגמה לשימוש |
__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]
בואו נבדוק איך זה עובד:
-
אחסון: המטמון משתמש ב-
OrderedDict
כדי לאחסן זוגות מפתח-ערך יחד עם ציוני זמן. -
תפוגה: כל ערך מאוחסן כזוגת
(ערך, זמן סימון)
. כאשר נגישים לערך, אנו בודקים האם הוא פג תוקפו. -
שיטות מיכל: המחלקה מיישמת את כל השיטות הנחוצות כדי להתנהג דומה למילון:
-
__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
מימוש המטמון הזה מספק מספר יתרונות:
-
פגיעה אוטומטית של רשומות ישנות
-
ממשק דומה למילון לשימוש קל
-
יעילות זיכרון על ידי הסרת רשומות שפגו
-
פעולות בטוחות לסלילה (בהנחה על גישה חד-תהליך)
-
שמירה על סדר ההכנסה של הרשומות
גישת מאפיינים
אמצעי גישה למאפיינים מאפשרים לך לשלוט באופן בו העצמים שלך מתמודדים עם קבלת, הגדרה ומחיקת מאפיינים. זה מועיל במיוחד ליישום מאפיינים, אימות ולוגים.
getattr ו־getattribute
פייתון מספקת שני שיטות לשליטה בגישה למאפיינים:
-
__getattr__
: נקראת רק כאשר חיפוש מאפיין נכשל (כלומר, כאשר המאפיין לא קיים) -
__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
באופן דומה, ניתן לשלוט באופן בו מאפיינים מוגדרים ונמחקים:
-
__setattr__
: נקרא כאשר מאפיין מתעדכן -
__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}'")
בואו נפרק איך המחלקה הזו פועלת:
-
אחסון: המחלקה משתמשת במילון פרטי
_data
כדי לאחסן ערכי מאפיינים. -
גישה למאפיינים:
-
__getattr__
: מחזיר ערכים מ-_data
ומרשום הודעות דיבוג -
__setattr__
: מאחסן ערכים ב-_data
ומרשום שינויים -
__delattr__
: מסיר ערכים מ-_data
ומרשום מחיקות
-
-
טיפול מיוחד: המאפיין
_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'
יישום זה מספק מספר יתרונות:
-
יומנים אוטומטיים של כל שינויי התכונות
-
יומני רמת דיבאג לגישה לתכונות
-
הודעות שגיאה ברורות עבור תכונות חסרות
-
ניטור קל של שינויים במצב האובייקט
-
מועיל לדיבאג ולביקורת
מנגד הקשר
סוגי מערכות ניהול הקרנות הם תכונה עוצמתית בפייתון שעוזרת לך לנהל משאבים בצורה נכונה. הם מבטיחים כי המשאבים יוכלו להיקבל ולהשוחרר בצורה נכונה, גם אם אירעה שגיאה. ההצהרה with
היא הדרך הנפוצה ביותר להשתמש במנהלי הקרנות.
הכניסה והיציאה
כדי ליצור מנהל קרנות, עליך ליישם שני מתודות קסם:
-
__enter__
: נקרא כאשר נכנסים לתוך בלוק ה-with
. עליו להחזיר את המשאב שיש לנהלו. -
__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
בואו נפרק איך מנהל ההקשר הזה עובד:
-
אתחול:
-
המחלקה מקבלת את נתיב מסד הנתונים
-
היא מאתחלת את החיבור והסדר כאפס
-
-
שיטת הכניסה:
-
יוצרת חיבור למסד נתונים
-
יוצרת סדר
-
מחזירה את הסדר לשימוש בבלוק
with
-
-
שיטת יציאה:
-
מנהל טיפול בעסקאות (אישור/ביטול)
-
סוגר עקומית וחיבור
-
מספר פעולות בלוג
-
מחזיר 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}")
מנהל ההקשר הזה מספק מספר יתרונות:
-
משאבים ניהלים אוטומטית (לדוגמא: חיבורים תמיד נסגרים).
-
עם ביטחון עסקאות, השינויים מכוסים או מתוקפלים באופן תקין.
-
חריגות נתפסות ומטופלות בצורה נאותה
-
כל הפעולות מתעדות לצורך איתור באגים
-
ההוראה
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}")
בואו ננתח כיצד דקורטור הממוּזציה הזה עובד:
-
אתחול:
-
מקבל פונקציה כארגומנט
-
יוצר מילון מטמון לאחסון תוצאות
-
שומר את המטאדאטה של הפונקציה בעזרת
functools.update_wrapper
-
-
קריאת שיטה:
-
יוצר מפתח ייחודי מארגומנטי הפונקציה
-
בודק אם התוצאה נמצאת במטמון
-
אם לא, מחשב את התוצאה ושומר אותה
-
מחזיר את התוצאה מהמטמון
-
-
שימוש:
-
מיושם כדקורטור לכל פונקציה
-
מטמן אוטומטית תוצאות לקריאות חוזרות
-
שומר על מטא-נתונים והתנהגות הפונקציה
-
היתרונות של המימוש כוללים:
-
ביצועים טובים יותר, משום שממנע חישובים מיותרים
-
טוב יותר, שקיפות, מכיוון שהוא עובד בלי לשנות את הפונקציה המקורית
-
זה גמיש וניתן לשימוש עם כל פונקציה
-
זה יעיל מבחינת זיכרון ומאפשר אחסון תוצאות לשימוש חוזר
-
זה שומר על תיעוד הפונקציה
שיטות קסם מתקדמות
כעת נבחן כמה מהשיטות הקסם המתקדמות יותר של פייתון. שיטות אלה מעניקות לך שליטה מדודה על יצירת אובייקט, שימוש בזיכרון והתנהגות במילון.
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 (האיניציאליזציה השנייה דרסה את הראשונה)
בואו נפרט כיצד פעולת הסינגלטון הזו עובדת:
-
משתנה מחלקה:
_instance
שומר על המופע היחיד של המחלקה -
שיטת new:
-
בודקת האם קיים מופע
-
יוצרת אחד אם לא קיים
-
מחזירה את המופע הקיים אם קיים
-
-
שיטת 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__
מגיע מתוך:
-
השלבת המילונים: עצמים רגילים בפייתון שומרים את התכונות שלהם במילון (
__dict__
), שיש לו עלויות. פונקצייתsys.getsizeof()
לא מתייחסת לגודל של המילון הזה. -
שמירת תכונות: במופעים קטנים עם מספר תכונות מעט, העלויות של תיאורי החריצים עשויות לעקוף את חיסכון המילון.
-
קיימות: היתרון האמיתי מופיע כאשר:
-
יש לך הרבה מופעים (אלפיים או מיליונים)
-
האובייקטים שלך מכילים הרבה מאפיינים
-
אתה מוסיף מאפיינים באופן דינמי
-
בואו נראה השוואה מלאה יותר:
# מדידת זיכרון מדויקת יותר
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__
:
-
יתרונות זיכרון אמיתיים: החיסכון בזיכרון העיקרי מגיע מהסרת המילון המופע
-
הגבלות דינמיות: לא ניתן להוסיף מאפיינים שרירותיים לאובייקטים שהוצמדו
-
שיקולי מורשת: שימוש ב־
__slots__
עם מורשת מחייב תכנון זהיר -
מקרי שימוש: מתאים ביותר למחלקות עם הרבה מופעים ומאפיינים קבועים
-
בונוס ביצועים: יכול גם לספק גישה מהירה יותר למאפיינים במקרים מסוימים
חסר לערכי מחרוזת ברירת מחדל
השיטה __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': ['ויבה']}
מימוש זה מספק מספר יתרונות:
-
אין צורך לבדוק האם מפתח קיים, מה שנוח יותר.
-
האתחול האוטומטי יוצר ערכי ברירת מחדל כפי שנדרש.
-
מפחית את הקוד המייתר באתחול של מילון.
-
זה גמיש יותר, ויכול ליישם כל לוגיקת ערך ברירת המחדל.
-
יוצר ערכים רק כאשר נדרש, ובכך מפחית את צריכת הזיכרון.
התחשבויות ביצועים
אף על פי ששיטות המג'יק עוצמתיות, הן עשויות להשפיע על ביצועים אם לא משתמשים בהן בצורה זהירה. בואו נבחן כמה התחשבויות ביצועים נפוצות ואיך למדוד אותן.
השפעת שיטות המג'יק על הביצועים
לשיטות המג'יק השונות יש השפעות שונות על הביצועים:
שיטות גישה למאפיינים:
-
__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__
איטי יותר מפי שניים בהשוואה לגישה ישירה למאפיין. זה עשוי לא להיות חשוב עבור מאפיינים שניגשים אליהם מדי פעם, אך זה יכול להפוך למשמעותי בקוד קריטי לביצועים שניגש למאפיינים בלולאות הדוקות.
אסטרטגיות אופטימיזציה
למזלנו, ישנן דרכים שונות שאתה יכול לייעל את המתודות הקסומות.
-
השתמש בשדות עבור יעילות זיכרון: זה מפחית את השימוש בזיכרון ומשפר את מהירות הגישה למאפיינים. זה הטוב ביותר עבור מחלקות עם הרבה מופעים.
-
שמרו ערכים מחושבים : אתם יכולים לאחסן תוצאות של פעולות יקרות ולעדכן את המטמון רק כשיש צורך. השתמשו ב
@property
עבור אטריבוטים מחושבים. -
מזערו קריאות מתודות : ודאו שאתם נמנעים מקריאות מתודות קסומות מיותרות והשתמשו בגישה ישירה לאטריבוטים כשאפשר. שקלו להשתמש ב
__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.
נקודות מפתח
-
ייצוג אובייקט:
-
השתמשו ב-
__str__
לפלט ידידותי למשתמש -
השתמשו ב-
__repr__
עבור דיבוג ופיתוח
-
-
עומס המפעילים:
-
יישום מפעילי חשבון והשוואה
-
החזרת
NotImplemented
לפעולות שאינן נתמכות -
השתמש ב־
@total_ordering
להשוואות עקביות
-
-
התנהגות המיכל:
-
יישום פרוטוקולי רצף ומיפוי
-
שיקול ביצוע לפעולות הנפוצות
-
טיפול במקרים קצה באופן הולם
-
-
ניהול משאבים:
-
השתמש במנהלי הקשר לטיפול נכון במשאבים
-
יישם
__enter__
ו-__exit__
לניקוי -
טפל בשגיאות ב-
__exit__
-
-
אופטימיזציה של ביצועים:
-
השתמש ב-
__slots__
ליעילות זיכרון -
אחסן ערכים מחושבים כאשר יש צורך
-
מזער קריאות מתודיות בקוד בשימוש תדיר
-
מתי להשתמש בשיטות קסם
שיטות קסם שימושיות במיוחד כשאתה צריך:
-
צור מבני נתונים מותאמים אישית
-
מממש סוגי דומיין מיוחדים
-
נהל משאבים בצורה תקינה
-
הוסף התנהגות מיוחדת למחלקות שלך
-
שפר את הקוד שלך כך שיהיה פייתוני יותר
מתי להימנע משימוש בשיטות קסם
הימנע משימוש בשיטות קסם כאשר:
-
גישה פשוטה למאפיינים מספיקה
-
ההתנהגות תהיה מבלבלת או בלתי צפויה
-
ביצועים מכריעים ושימוש בשיטות קסם יוסיף עומס
-
המימוש יהיה מורכב מדי
זכור שעם כוח גדול מגיעה אחריות גדולה. השתמש בשיטות קסם בצורה יועילה, תוך שמירה על השפעות הביצועים שלהן ועל עקרון הפחד הכי קטן. כאשר משתמשים בהן בצורה נכונה, שיטות הקסם יכולות לשפר באופן משמעותי את הקריאות והביטויות של הקוד שלך.
הפניות וקריאה נוספת
מסמכי הפייתון הרשמיים
-
Python מודל נתונים – תיעוד רשמי – מדריך מקיף למודל הנתונים ולשיטות הקסם של פייתון.
-
functools.total_ordering – תיעוד עבור הקישוט total_ordering שממלא אוטומטית שיטות השוואה חסרות.
-
שמות שיטות מיוחדות בפייתון – מדריך רשמי לזהויות של שיטות מיוחדות בפייתון.
-
מחלקות המקור המופשט של אוספים – למידה על מחלקות המקור המופשט של תופעות שמגדירות את הממשקים שאפשר לממש במחלקות האוסף שלך.
משאבי קהילה
- מדריך לשיטות הקסם של פייתון – רייף קטלר – דוגמאות מעשיות לשיטות הקסם ולמקרים נפוצים של שימוש.
קריאה נוספת
אם נהניתם מהמאמר, ייתכן שתמצאו את המאמרים הבאים הקשורים לפייתון בבלוג האישי שלי:
-
ניסויים מעשיים לאופטימיזציות שאילתות ORM של Django – למידה כיצד לאופטימז פניות ORM של Django שלך עם דוגמאות מעשיות וניסויים.
-
העלות הגבוהה של עיבוד סנכרוני ב־uWSGI – הבנת ההשפעות על ביצועים של עיבוד סנכרוני ב־uWSGI וכיצד זה משפיע על אפליקציות האינטרנט שלך בפייתון.
Source:
https://www.freecodecamp.org/news/python-magic-methods-practical-guide/