Ostatnio zadano mi to samo pytanie i otrzymałem kilka odpowiedzi. Mam nadzieję, że mogę ożywić ten wątek, ponieważ chciałem rozwinąć kilka z wymienionych przypadków użycia i dodać kilka nowych.
Większość metaklas, które widziałem, robi jedną z dwóch rzeczy:
Rejestracja (dodanie klasy do struktury danych):
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
Za każdym razem, gdy tworzysz podklasę Model
, Twoja klasa jest rejestrowana w models
słowniku:
>>> class A(Model):
... pass
...
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...>,
'B': <__main__.B class at 0x...>}
Można to również zrobić za pomocą dekoratorów klas:
models = {}
def model(cls):
models[cls.__name__] = cls
return cls
@model
class A(object):
pass
Lub z wyraźną funkcją rejestracji:
models = {}
def register_model(cls):
models[cls.__name__] = cls
class A(object):
pass
register_model(A)
Właściwie jest to prawie to samo: niepomyślnie wspominasz o dekoratorach klas, ale tak naprawdę jest to nic innego jak cukier syntaktyczny dla wywołania funkcji w klasie, więc nie ma w tym żadnej magii.
W każdym razie zaletą metaklas w tym przypadku jest dziedziczenie, ponieważ działają one dla dowolnych podklas, podczas gdy inne rozwiązania działają tylko dla podklas jawnie ozdobionych lub zarejestrowanych.
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...> # No B :(
Refaktoryzacja (modyfikacja atrybutów klas lub dodanie nowych):
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
Za każdym razem, gdy podklasujesz Model
i definiujesz niektóre Field
atrybuty, są one wstrzykiwane wraz z ich nazwami (na przykład w celu uzyskania bardziej szczegółowych komunikatów o błędach) i grupowane w _fields
słowniku (dla łatwej iteracji, bez konieczności przeglądania wszystkich atrybutów klasy i wszystkich jej klas podstawowych '' atrybuty za każdym razem):
>>> class A(Model):
... foo = Integer()
...
>>> class B(A):
... bar = String()
...
>>> B._fields
{'foo': Integer('A.foo'), 'bar': String('B.bar')}
Ponownie, można to zrobić (bez dziedziczenia) za pomocą dekoratora klas:
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')} :(
Lub wyraźnie:
class A(object):
foo = Integer('A.foo')
_fields = {'foo': foo} # Don't forget all the base classes' fields, too!
Chociaż, w przeciwieństwie do twojego poparcia dla czytelnego i łatwego w utrzymaniu programowania niemetatowego, jest to znacznie bardziej uciążliwe, nadmiarowe i podatne na błędy:
class B(A):
bar = String()
# vs.
class B(A):
bar = String('bar')
_fields = {'B.bar': bar, 'A.foo': A.foo}
Biorąc pod uwagę najbardziej powszechne i konkretne przypadki użycia, jedynymi przypadkami, w których absolutnie MUSISZ używać metaklas, są sytuacje, gdy chcesz zmodyfikować nazwę klasy lub listę klas bazowych, ponieważ po zdefiniowaniu parametry te są wstawiane do klasy i nie ma dekoratora lub funkcja może je rozpalić.
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
Może to być przydatne w frameworkach do wydawania ostrzeżeń, gdy zdefiniowane są klasy o podobnych nazwach lub niepełnych drzewach dziedziczenia, ale nie mogę wymyślić powodu poza trollowaniem, aby faktycznie zmienić te wartości. Może David Beazley może.
W każdym razie w Pythonie 3 metaklasy mają również __prepare__
metodę, która pozwala oszacować treść klasy na odwzorowanie inne niż a dict
, obsługując w ten sposób uporządkowane atrybuty, przeciążone atrybuty i inne niesamowite fajne rzeczy:
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...>
Możesz argumentować, że uporządkowane atrybuty można osiągnąć za pomocą liczników tworzenia, a przeciążenie można symulować za pomocą domyślnych argumentów:
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)
Poza tym, że jest dużo bardziej brzydki, jest również mniej elastyczny: co jeśli chcesz uporządkować atrybuty dosłowne, takie jak liczby całkowite i łańcuchy? A co, jeśli None
jest to prawidłowa wartość x
?
Oto kreatywny sposób rozwiązania pierwszego problemu:
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']
A oto kreatywny sposób na rozwiązanie drugiego:
_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)
Ale to dużo, DUŻO voodoo-er niż zwykła metaklasa (zwłaszcza pierwsza, która naprawdę topi twój mózg). Chodzi mi o to, że patrzysz na metaklasy jako nieznane i sprzeczne z intuicją, ale możesz też spojrzeć na nie jako na kolejny krok ewolucji języków programowania: po prostu musisz dostosować swój sposób myślenia. W końcu mógłbyś prawdopodobnie zrobić wszystko w C, w tym zdefiniować strukturę ze wskaźnikami do funkcji i przekazać ją jako pierwszy argument do jej funkcji. Osoba, która zobaczy C ++ po raz pierwszy, może powiedzieć: „co to za magia? Dlaczego kompilator niejawnie przechodzithis
do metod, ale nie do funkcji regularnych i statycznych? Lepiej jest mówić wprost i rozwlekle w swoich argumentach. ”Ale z drugiej strony programowanie zorientowane obiektowo jest znacznie potężniejsze, kiedy już je zrozumiesz; i tak samo jest z tym, uh ... programowaniem quasi-aspektowym, jak sądzę. rozumiesz metaklasy, w rzeczywistości są one bardzo proste, więc dlaczego nie używać ich, gdy jest to wygodne?
I wreszcie metaklasy są świetne, a programowanie powinno być zabawne. Korzystanie ze standardowych konstrukcji programistycznych i wzorców projektowych przez cały czas jest nudne i mało inspirujące oraz ogranicza wyobraźnię. Żyć trochę! Oto metametaklasa specjalnie dla Ciebie.
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
Edytować
To dość stare pytanie, ale wciąż otrzymuję głosy za, więc pomyślałem, że dodam link do bardziej wyczerpującej odpowiedzi. Jeśli chcesz przeczytać więcej o metaklasach i ich zastosowaniach, właśnie opublikowałem tutaj artykuł na ten temat .