~/icsd.ir — bash
SYSTEM_ONLINE

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

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.