Нещодавно мені поставили те саме питання, і я придумав кілька відповідей. Я сподіваюся, що це нормально, щоб відновити цю тему, оскільки я хотів детальніше розглянути декілька згаданих випадків використання та додати кілька нових.
Більшість метакласів, яких я бачив, роблять одну з двох речей:
Реєстрація (додавання класу до структури даних):
models = {}
class ModelMetaclass(type):
def __new__(meta, name, bases, attrs):
models[name] = cls = type.__new__(meta, name, bases, attrs)
return cls
class Model(object):
__metaclass__ = ModelMetaclass
Щоразу, коли ви підклас Model
, ваш клас реєструється у models
словнику:
>>> class A(Model):
... pass
...
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...>,
'B': <__main__.B class at 0x...>}
Це також можна зробити з декораторами класу:
models = {}
def model(cls):
models[cls.__name__] = cls
return cls
@model
class A(object):
pass
Або з чіткою функцією реєстрації:
models = {}
def register_model(cls):
models[cls.__name__] = cls
class A(object):
pass
register_model(A)
Насправді це майже те саме: ви згадуєте декораторів класів недоброзичливо, але це насправді не що інше, як синтаксичний цукор для виклику функції класу, тому в цьому немає ніякої магії.
У будь-якому випадку перевагою метакласів у цьому випадку є успадкування, оскільки вони працюють для будь-яких підкласів, тоді як інші рішення працюють лише для підкласів, явно оформлених або зареєстрованих.
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...> # No B :(
Рефакторинг (зміна атрибутів класу або додавання нових):
class ModelMetaclass(type):
def __new__(meta, name, bases, attrs):
fields = {}
for key, value in attrs.items():
if isinstance(value, Field):
value.name = '%s.%s' % (name, key)
fields[key] = value
for base in bases:
if hasattr(base, '_fields'):
fields.update(base._fields)
attrs['_fields'] = fields
return type.__new__(meta, name, bases, attrs)
class Model(object):
__metaclass__ = ModelMetaclass
Щоразу, коли ви підклас Model
і визначаєте деякі Field
атрибути, вони вводяться їхні імена (наприклад, для отримання більш інформативних повідомлень про помилки) та групуються у _fields
словник (для легкої ітерації, не переглядаючи всі атрибути класу та всі його базові класи ' атрибути щоразу):
>>> class A(Model):
... foo = Integer()
...
>>> class B(A):
... bar = String()
...
>>> B._fields
{'foo': Integer('A.foo'), 'bar': String('B.bar')}
Знову ж таки, це можна зробити (без успадкування) за допомогою декоратора класу:
def model(cls):
fields = {}
for key, value in vars(cls).items():
if isinstance(value, Field):
value.name = '%s.%s' % (cls.__name__, key)
fields[key] = value
for base in cls.__bases__:
if hasattr(base, '_fields'):
fields.update(base._fields)
cls._fields = fields
return cls
@model
class A(object):
foo = Integer()
class B(A):
bar = String()
# B.bar has no name :(
# B._fields is {'foo': Integer('A.foo')} :(
Або явно:
class A(object):
foo = Integer('A.foo')
_fields = {'foo': foo} # Don't forget all the base classes' fields, too!
Хоча, на противагу вашій пропаганді читабельного та бездоганного неметативного програмування, це набагато громіздкіше, надмірне та схильне до помилок:
class B(A):
bar = String()
# vs.
class B(A):
bar = String('bar')
_fields = {'B.bar': bar, 'A.foo': A.foo}
Розглянувши найпоширеніші та конкретні випадки використання, єдиними випадками, коли ви абсолютно ДОБРИМИ використовувати метакласи, - це коли ви хочете змінити назву класу або список базових класів, тому що після їх визначення ці параметри вводяться в клас, і немає декоратора або функція може скасувати їх.
class Metaclass(type):
def __new__(meta, name, bases, attrs):
return type.__new__(meta, 'foo', (int,), attrs)
class Baseclass(object):
__metaclass__ = Metaclass
class A(Baseclass):
pass
class B(A):
pass
print A.__name__ # foo
print B.__name__ # foo
print issubclass(B, A) # False
print issubclass(B, int) # True
Це може бути корисно в рамках для видачі попереджень щоразу, коли визначаються класи з подібними іменами або неповними деревами спадкування, але я не можу придумати, крім того, щоб тролінг насправді змінив ці значення. Може, Девід Бізлі може.
У будь-якому випадку в Python 3 метакласи також мають __prepare__
метод, який дозволяє оцінити тіло класу на відображення, відмінне від a dict
, таким чином підтримуючи впорядковані атрибути, перевантажені атрибути та інші злі класні речі:
import collections
class Metaclass(type):
@classmethod
def __prepare__(meta, name, bases, **kwds):
return collections.OrderedDict()
def __new__(meta, name, bases, attrs, **kwds):
print(list(attrs))
# Do more stuff...
class A(metaclass=Metaclass):
x = 1
y = 2
# prints ['x', 'y'] rather than ['y', 'x']
class ListDict(dict):
def __setitem__(self, key, value):
self.setdefault(key, []).append(value)
class Metaclass(type):
@classmethod
def __prepare__(meta, name, bases, **kwds):
return ListDict()
def __new__(meta, name, bases, attrs, **kwds):
print(attrs['foo'])
# Do more stuff...
class A(metaclass=Metaclass):
def foo(self):
pass
def foo(self, x):
pass
# prints [<function foo at 0x...>, <function foo at 0x...>] rather than <function foo at 0x...>
Ви можете стверджувати, що впорядковані атрибути можна досягти за допомогою лічильників створення, а перевантаження можна імітувати аргументами за замовчуванням:
import itertools
class Attribute(object):
_counter = itertools.count()
def __init__(self):
self._count = Attribute._counter.next()
class A(object):
x = Attribute()
y = Attribute()
A._order = sorted([(k, v) for k, v in vars(A).items() if isinstance(v, Attribute)],
key = lambda (k, v): v._count)
class A(object):
def _foo0(self):
pass
def _foo1(self, x):
pass
def foo(self, x=None):
if x is None:
return self._foo0()
else:
return self._foo1(x)
Окрім того, що він набагато некрасивіший, він також менш гнучкий: що робити, якщо ви хочете впорядковані буквальні атрибути, такі як цілі числа та рядки? Що робити, якщо None
це дійсне значення x
?
Ось творчий спосіб вирішити першу проблему:
import sys
class Builder(object):
def __call__(self, cls):
cls._order = self.frame.f_code.co_names
return cls
def ordered():
builder = Builder()
def trace(frame, event, arg):
builder.frame = frame
sys.settrace(None)
sys.settrace(trace)
return builder
@ordered()
class A(object):
x = 1
y = 'foo'
print A._order # ['x', 'y']
І ось творчий спосіб вирішити другий:
_undefined = object()
class A(object):
def _foo0(self):
pass
def _foo1(self, x):
pass
def foo(self, x=_undefined):
if x is _undefined:
return self._foo0()
else:
return self._foo1(x)
Але це набагато, МНОГО вуду-ер, ніж простий метаклас (особливо перший, який справді розтоплює ваш мозок). Моя думка, ви дивитесь на метакласи як на незнайомі та контрінтуїтивні, але ви також можете розглядати їх як наступний крок еволюції в мовах програмування: вам просто потрібно налаштувати свій розум. Зрештою, ви могли, ймовірно, робити все на C, включаючи визначення структури з функціональними вказівниками та передачу її як перший аргумент його функціям. Людина, яка вперше бачить C ++, може сказати: "що це за магія? Чому компілятор неявно проходитьthis
до методів, але не до регулярних і статичних функцій? Краще бути чітким і детальним щодо своїх аргументів ". Але тоді, об'єктно-орієнтоване програмування стає набагато потужнішим, як тільки ви його отримуєте; і так це, е ... квазі-аспект-орієнтоване програмування, я думаю, і колись ви розумієте метакласи, вони насправді дуже прості, так чому б не використовувати їх, коли зручно?
І нарешті, метакласи - це рад, а програмування має бути цікавим. Використання стандартних конструкцій програмування та моделей дизайну весь час нудне і не надихає, і заважає вашій уяві. Живи трохи! Ось метаметаклас, тільки для вас.
class MetaMetaclass(type):
def __new__(meta, name, bases, attrs):
def __new__(meta, name, bases, attrs):
cls = type.__new__(meta, name, bases, attrs)
cls._label = 'Made in %s' % meta.__name__
return cls
attrs['__new__'] = __new__
return type.__new__(meta, name, bases, attrs)
class China(type):
__metaclass__ = MetaMetaclass
class Taiwan(type):
__metaclass__ = MetaMetaclass
class A(object):
__metaclass__ = China
class B(object):
__metaclass__ = Taiwan
print A._label # Made in China
print B._label # Made in Taiwan
Редагувати
Це досить давнє запитання, але воно все ще отримує підсумки, тому я подумав би додати посилання на більш вичерпну відповідь. Якщо ви хочете прочитати більше про метакласи та їх використання, я щойно опублікував статтю про це тут .