Magic Methods و Operator Overloading
Magic Methods (یا Dunder Methods - Double Underscore Methods) متدهایی هستند که با دو زیرخط شروع و پایان مییابند و رفتار built-in پایتون را برای کلاسهای ما تعریف میکنند. در این فصل، با مهمترین magic methods و Operator Overloading آشنا میشویم.
Magic Methods (یا Dunder Methods – Double Underscore Methods) متدهایی هستند که با دو زیرخط شروع و پایان مییابند و رفتار built-in پایتون را برای کلاسهای ما تعریف میکنند. در این فصل، با مهمترین magic methods و Operator Overloading آشنا میشویم.
معرفی Magic Methods
Magic methods به ما اجازه میدهند کلاسهایمان مثل انواع built-in رفتار کنند:
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
book = Book("شاهنامه", 1500)
print(book) # <__main__.Book object at 0x...> (نهچندان مفید)
print(len(book)) # TypeError - Book نمیداند len چیست
# با magic methods رفع میشود
__str__ و __repr__
دو متد برای نمایش متنی شیء:
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __str__(self):
"""نمایش کاربرپسند - برای print()"""
return f"{self.title} نوشته {self.author}"
def __repr__(self):
"""نمایش برای توسعهدهنده - دقیق و قابل بازسازی"""
return f"Book(title={self.title!r}, author={self.author!r}, pages={self.pages})"
book = Book("شاهنامه", "فردوسی", 1500)
print(book) # شاهنامه نوشته فردوسی
print(repr(book)) # Book(title='شاهنامه', author='فردوسی', pages=1500)
# در REPL
book # خروجی repr
__repr__ باید unambiguous باشد، __str__ باید readable. اگر فقط یکی پیاده میکنید، __repr__ را پیاده کنید (پایتون از آن بهعنوان fallback برای __str__ استفاده میکند).
عملگرهای مقایسه
class Money:
def __init__(self, amount, currency="IRR"):
self.amount = amount
self.currency = currency
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.amount == other.amount and self.currency == other.currency
def __lt__(self, other): # less than
return self.amount < other.amount
def __le__(self, other): # less or equal
return self.amount <= other.amount
def __gt__(self, other): # greater than
return self.amount > other.amount
def __ge__(self, other): # greater or equal
return self.amount >= other.amount
def __hash__(self):
# برای استفاده در set و dict
return hash((self.amount, self.currency))
m1 = Money(1000)
m2 = Money(2000)
print(m1 == m2) # False
print(m1 < m2) # True
print(m1 != m2) # True (پایتون خودش از __eq__ استفاده میکند)
functools.total_ordering
برای کاهش boilerplate، فقط __eq__ و __lt__ را پیاده کنید:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor):
self.major = major
self.minor = minor
def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)
# total_ordering خودکار __le__, __gt__, __ge__ را میسازد
v1 = Version(3, 10)
v2 = Version(3, 12)
print(v1 < v2) # True
print(v1 >= v2) # False (خودکار ساخته شد)
عملگرهای ریاضی
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other): # +
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other): # -
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar): # *
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # برای 2 * vector
return self.__mul__(scalar)
def __neg__(self): # -vector
return Vector(-self.x, -self.y)
def __abs__(self): # abs(vector) - طول بردار
return (self.x ** 2 + self.y ** 2) ** 0.5
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v2 - v1) # Vector(2, 2)
print(v1 * 3) # Vector(3, 6)
print(2 * v1) # Vector(2, 4) - از __rmul__
print(-v1) # Vector(-1, -2)
print(abs(v2)) # 5.0
عملگرهای اختصاص (in-place)
class Counter:
def __init__(self, value=0):
self.value = value
def __iadd__(self, other): # +=
self.value += other
return self
def __isub__(self, other): # -=
self.value -= other
return self
c = Counter(10)
c += 5
print(c.value) # 15
کلاسهای کانتینری
برای ساخت کلاسی که مثل لیست/دیکشنری رفتار کند:
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def __len__(self):
"""len(playlist)"""
return len(self.songs)
def __getitem__(self, index):
"""playlist[i]"""
return self.songs[index]
def __setitem__(self, index, value):
"""playlist[i] = value"""
self.songs[index] = value
def __delitem__(self, index):
"""del playlist[i]"""
del self.songs[index]
def __contains__(self, song):
"""song in playlist"""
return song in self.songs
def __iter__(self):
"""for song in playlist"""
return iter(self.songs)
def add(self, song):
self.songs.append(song)
pl = Playlist("Favorites")
pl.add("سلطان قلبها")
pl.add("ایران ایران")
pl.add("بوی جوی مولیان")
print(len(pl)) # 3
print(pl[0]) # سلطان قلبها
print("ایران ایران" in pl) # True
for song in pl: # __iter__
print(song)
del pl[1]
print(len(pl)) # 2
__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
# کاربرد در دکوراتورها و factory functions
print(callable(double)) # True
__enter__ و __exit__ (Context Manager)
class Timer:
"""Context manager که زمان اجرا را اندازهگیری میکند"""
def __enter__(self):
import time
self.start = time.time()
return self
def __exit__(self, exc_type, exc_value, traceback):
import time
self.elapsed = time.time() - self.start
print(f"زمان اجرا: {self.elapsed:.3f} ثانیه")
return False # exception را propagate کن
# استفاده با with
with Timer() as t:
sum(range(10_000_000))
# زمان اجرا: 0.156 ثانیه
__hash__ و قاعده تساوی
__eq__ را تعریف میکنید، باید __hash__ را هم تعریف کنید (یا __hash__ = None برای غیر hashable).
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
# حالا قابل استفاده در set و dict
points = {Point(1, 2), Point(3, 4), Point(1, 2)}
print(len(points)) # 2 (تکراری حذف شد)
__format__ – قالببندی سفارشی
class Currency:
def __init__(self, amount):
self.amount = amount
def __format__(self, spec):
if spec == "":
return f"{self.amount} تومان"
elif spec == "fa":
# تبدیل به اعداد فارسی
num = f"{self.amount:,}"
fa_digits = "۰۱۲۳۴۵۶۷۸۹"
return num.translate(str.maketrans("0123456789", fa_digits)) + " تومان"
elif spec == "short":
return f"{self.amount/1000:.0f}K تومان"
return str(self.amount)
c = Currency(1500000)
print(f"{c}") # 1500000 تومان
print(f"{c:fa}") # ۱,۵۰۰,۰۰۰ تومان
print(f"{c:short}") # 1500K تومان
مثال جامع: کلاس Money
from functools import total_ordering
@total_ordering
class Money:
def __init__(self, amount, currency="IRR"):
self.amount = amount
self.currency = currency
def _check_currency(self, other):
if self.currency != other.currency:
raise ValueError(f"واحد متفاوت: {self.currency} vs {other.currency}")
def __add__(self, other):
self._check_currency(other)
return Money(self.amount + other.amount, self.currency)
def __sub__(self, other):
self._check_currency(other)
return Money(self.amount - other.amount, self.currency)
def __mul__(self, factor):
return Money(self.amount * factor, self.currency)
def __eq__(self, other):
return self.amount == other.amount and self.currency == other.currency
def __lt__(self, other):
self._check_currency(other)
return self.amount < other.amount
def __hash__(self):
return hash((self.amount, self.currency))
def __str__(self):
return f"{self.amount:,} {self.currency}"
def __repr__(self):
return f"Money({self.amount}, {self.currency!r})"
price = Money(1_000_000)
tax = Money(90_000)
total = price + tax
print(total) # 1,090,000 IRR
print(total > price) # True
discount = total * 0.9
print(discount) # 981,000.0 IRR
جمعبندی
- Magic methods رفتار built-in پایتون را برای کلاسهای شما فعال میکنند
__str__برای نمایش کاربری،__repr__برای دیباگ- عملگرهای ریاضی و مقایسه را با magic methods overload کنید
- برای کانتینرها:
__len__،__getitem__،__iter__،__contains__ __call__شیء را قابل فراخوانی میکند- اگر
__eq__دارید،__hash__هم لازم است