Metaclasses و Descriptors
Metaclasses و Descriptors از پیشرفتهترین مباحث پایتون هستند که اجازه میدهند رفتار خود کلاسها (نه فقط نمونههایشان) را کنترل کنیم. در این فصل با type، __new__، metaclass، descriptor protocol و کاربردهای واقعی آنها در فریمورکهای بزرگ آشنا میشویم.
Metaclasses و Descriptors از پیشرفتهترین مباحث پایتون هستند که اجازه میدهند رفتار خود کلاسها (نه فقط نمونههایشان) را کنترل کنیم. در این فصل با type، __new__، metaclass، descriptor protocol و کاربردهای واقعی آنها در فریمورکهای بزرگ آشنا میشویم.
type – تابع و کلاس
type در پایتون دو نقش دارد:
# 1) برای دانستن نوع
print(type(5)) # <class 'int'>
print(type("hello")) # <class 'str'>
print(type([])) # <class 'list'>
# 2) برای ساخت کلاس بهصورت پویا
# سینتکس: type(name, bases, dict)
MyClass = type("MyClass", (object,), {"x": 10, "greet": lambda self: "hi"})
obj = MyClass()
print(obj.x) # 10
print(obj.greet()) # hi
# معادل تعریف معمولی
class MyClass2:
x = 10
def greet(self):
return "hi"
Metaclass چیست؟
Metaclass، کلاسِ یک کلاس است. وقتی class Foo: مینویسید، پایتون از یک metaclass (بهطور پیشفرض type) برای ساخت آن کلاس استفاده میکند.
class Foo:
pass
print(type(Foo)) # <class 'type'> - Foo از type ساخته شده
print(type(Foo())) # <class 'Foo'>
print(type(type)) # <class 'type'> - type از خودش!
تعریف metaclass سفارشی
class MyMeta(type):
def __new__(mcs, name, bases, namespace):
print(f"ساخت کلاس {name}")
# دستکاری namespace قبل از ساخت
namespace["created_by_meta"] = True
return super().__new__(mcs, name, bases, namespace)
class MyClass(metaclass=MyMeta):
x = 10
# ساخت کلاس MyClass
print(MyClass.created_by_meta) # True
print(MyClass.x) # 10
کاربردهای Metaclass
۱. ثبت خودکار کلاسها
class PluginMeta(type):
plugins = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if name != "Plugin": # کلاس پایه را ثبت نکن
mcs.plugins[name] = cls
return cls
class Plugin(metaclass=PluginMeta):
pass
# هر کلاسی که از Plugin ارث ببرد خودکار ثبت میشود
class EmailPlugin(Plugin):
def send(self, msg):
print(f"ایمیل: {msg}")
class SMSPlugin(Plugin):
def send(self, msg):
print(f"پیامک: {msg}")
print(PluginMeta.plugins)
# {'EmailPlugin': <class 'EmailPlugin'>, 'SMSPlugin': <class 'SMSPlugin'>}
# استفاده داینامیک
plugin = PluginMeta.plugins["EmailPlugin"]()
plugin.send("سلام")
۲. اعتبارسنجی کلاس
class ValidatedMeta(type):
def __new__(mcs, name, bases, namespace):
# هر کلاس باید متد validate داشته باشد
if name != "Validated" and "validate" not in namespace:
raise TypeError(f"{name} باید متد validate داشته باشد")
return super().__new__(mcs, name, bases, namespace)
class Validated(metaclass=ValidatedMeta):
pass
class Form(Validated):
def validate(self):
return True
# class BadForm(Validated):
# pass # TypeError: BadForm باید متد validate داشته باشد
۳. ORM ساده (مثل Django)
class Field:
def __init__(self, field_type):
self.field_type = field_type
class ModelMeta(type):
def __new__(mcs, name, bases, namespace):
# جمعآوری فیلدها
fields = {
key: value for key, value in namespace.items()
if isinstance(value, Field)
}
namespace["_fields"] = fields
namespace["_table_name"] = name.lower()
return super().__new__(mcs, name, bases, namespace)
class Model(metaclass=ModelMeta):
@classmethod
def create_table_sql(cls):
cols = [f"{name} {field.field_type}" for name, field in cls._fields.items()]
return f"CREATE TABLE {cls._table_name} ({', '.join(cols)})"
class User(Model):
id = Field("INTEGER PRIMARY KEY")
name = Field("VARCHAR(100)")
email = Field("VARCHAR(255)")
print(User.create_table_sql())
# CREATE TABLE user (id INTEGER PRIMARY KEY, name VARCHAR(100), email VARCHAR(255))
Descriptor Protocol
Descriptor، شیئی است که حداقل یکی از این متدها را پیاده میکند:
__get__(self, instance, owner)__set__(self, instance, value)__delete__(self, instance)
Descriptor ساده
class TenTimes:
"""هر مقداری بگیرد، 10 برابر برمیگرداند"""
def __get__(self, instance, owner):
return self.value * 10
def __set__(self, instance, value):
self.value = value
class MyClass:
x = TenTimes()
obj = MyClass()
obj.x = 5
print(obj.x) # 50
Descriptor با اعتبارسنجی
class PositiveNumber:
"""فقط اعداد مثبت قبول میکند"""
def __set_name__(self, owner, name):
self.name = name # نام attribute
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, (int, float)):
raise TypeError(f"{self.name} باید عدد باشد")
if value <= 0:
raise ValueError(f"{self.name} باید مثبت باشد")
instance.__dict__[self.name] = value
class Product:
price = PositiveNumber()
quantity = PositiveNumber()
p = Product()
p.price = 1000 # OK
p.quantity = 5 # OK
# p.price = -100 # ValueError
# p.quantity = "x" # TypeError
Descriptor با کش
class CachedProperty:
"""مثل @property اما نتیجه را کش میکند"""
def __init__(self, func):
self.func = func
self.name = func.__name__
def __get__(self, instance, owner):
if instance is None:
return self
if self.name not in instance.__dict__:
instance.__dict__[self.name] = self.func(instance)
return instance.__dict__[self.name]
class DataAnalyzer:
def __init__(self, data):
self.data = data
@CachedProperty
def average(self):
print("محاسبه میانگین...")
return sum(self.data) / len(self.data)
a = DataAnalyzer([1, 2, 3, 4, 5])
print(a.average) # محاسبه میانگین... 3.0
print(a.average) # 3.0 (بدون محاسبه مجدد)
print(a.average) # 3.0 (کش)
# پایتون 3.8+ خود این را دارد
from functools import cached_property
property و descriptor
@property در واقع نمونهای از descriptor است!
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("زیر صفر مطلق")
self._celsius = value
# معادل با descriptor:
class CelsiusDescriptor:
def __get__(self, instance, owner):
return instance._celsius
def __set__(self, instance, value):
if value < -273.15:
raise ValueError("زیر صفر مطلق")
instance._celsius = value
class Temperature2:
celsius = CelsiusDescriptor()
def __init__(self, celsius=0):
self._celsius = celsius
Data vs Non-Data Descriptors
- Data Descriptor:
__set__یا__delete__دارد – اولویت برinstance.__dict__ - Non-Data Descriptor: فقط
__get__–instance.__dict__اولویت دارد
# Non-data descriptor (مثل @cached_property)
class NonData:
def __get__(self, instance, owner):
return "از descriptor"
class Foo:
x = NonData()
f = Foo()
print(f.x) # از descriptor
f.__dict__["x"] = "از instance"
print(f.x) # از instance ← این اولویت دارد
__init_subclass__ – جایگزین سادهتر
اغلب نیاز به metaclass واقعی نیست – __init_subclass__ کافی است:
class Plugin:
plugins = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.plugins[cls.__name__] = cls
class EmailPlugin(Plugin):
pass
class SMSPlugin(Plugin):
pass
print(Plugin.plugins)
# {'EmailPlugin': <class 'EmailPlugin'>, 'SMSPlugin': <class 'SMSPlugin'>}
# با پارامتر
class Plugin:
plugins = {}
def __init_subclass__(cls, *, name=None, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.plugins[name or cls.__name__] = cls
class MyPlugin(Plugin, name="custom"):
pass
print(Plugin.plugins) # {'custom': <class 'MyPlugin'>}
بهترین شیوهها
- قبل از metaclass، ببینید آیا
__init_subclass__یا decorator کافی است - “اگر مطمئن نیستید نیاز به metaclass دارید، نیاز ندارید” – Tim Peters
- Descriptor برای reusable validation و computed attributes
- property کافی است مگر بخواهید روی چند کلاس استفاده کنید
__set_name__برای دسترسی به نام attribute
چه زمانی استفاده کنیم؟
| هدف | راهحل |
|---|---|
| تغییر یک نمونه | __init__ |
| تغییر ساخت کلاس | decorator یا __init_subclass__ |
| تغییر همه subclasses | __init_subclass__ |
| تغییر اساسی نحوه ساخت کلاس | metaclass |
| کنترل دسترسی attribute | property یا descriptor |
جمعبندی
- type تابع و metaclass پایه است
- Metaclass برای کنترل ساخت کلاسها
- Descriptor protocol: __get__, __set__, __delete__
- property خود نوعی descriptor است
__init_subclass__اغلب جایگزین سادهتر metaclass- کاربرد در فریمورکها: ORM، Plugin system، DI container