Recentemente mi è stata posta la stessa domanda e ho trovato diverse risposte. Spero che sia OK per far rivivere questo thread, poiché volevo elaborare alcuni dei casi d'uso menzionati e aggiungerne alcuni nuovi.
La maggior parte delle metaclassi che ho visto fare una delle due cose:
Registrazione (aggiunta di una classe a una struttura dati):
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
Ogni volta che fai una sottoclasse Model
, la tua classe viene registrata nel models
dizionario:
>>> class A(Model):
... pass
...
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...>,
'B': <__main__.B class at 0x...>}
Questo può essere fatto anche con i decoratori di classe:
models = {}
def model(cls):
models[cls.__name__] = cls
return cls
@model
class A(object):
pass
Oppure con una funzione di registrazione esplicita:
models = {}
def register_model(cls):
models[cls.__name__] = cls
class A(object):
pass
register_model(A)
In realtà, è più o meno lo stesso: menzioni sfavorevolmente i decoratori di classe, ma in realtà non è altro che zucchero sintattico per un'invocazione di funzione su una classe, quindi non c'è magia al riguardo.
Ad ogni modo, il vantaggio delle metaclassi in questo caso è l'ereditarietà, poiché funzionano per eventuali sottoclassi, mentre le altre soluzioni funzionano solo per sottoclassi esplicitamente decorate o registrate.
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...> # No B :(
Refactoring (modifica degli attributi della classe o aggiunta di nuovi):
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
Ogni volta che si sottoclasse Model
e si definiscono alcuni Field
attributi, vengono iniettati i loro nomi (per messaggi di errore più informativi, ad esempio) e raggruppati in un _fields
dizionario (per una facile iterazione, senza dover guardare attraverso tutti gli attributi della classe e tutte le sue classi base ' attributi ogni volta):
>>> class A(Model):
... foo = Integer()
...
>>> class B(A):
... bar = String()
...
>>> B._fields
{'foo': Integer('A.foo'), 'bar': String('B.bar')}
Di nuovo, questo può essere fatto (senza ereditarietà) con un decoratore di classi:
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')} :(
O esplicitamente:
class A(object):
foo = Integer('A.foo')
_fields = {'foo': foo} # Don't forget all the base classes' fields, too!
Sebbene, al contrario della tua difesa di una meta programmazione leggibile e manutenibile, questa è molto più ingombrante, ridondante e soggetta a errori:
class B(A):
bar = String()
# vs.
class B(A):
bar = String('bar')
_fields = {'B.bar': bar, 'A.foo': A.foo}
Dopo aver considerato i casi d'uso più comuni e concreti, gli unici casi in cui DEVI assolutamente usare le metaclassi sono quando vuoi modificare il nome della classe o l'elenco delle classi base, perché una volta definiti, questi parametri sono inseriti nella classe e nessun decoratore o la funzione può sbloccarli.
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
Ciò può essere utile nei framework per l'emissione di avvisi ogni volta che vengono definite classi con nomi simili o alberi di ereditarietà incompleti, ma non riesco a pensare a un motivo oltre al troll per modificare effettivamente questi valori. Forse David Beazley può.
Ad ogni modo, in Python 3, le metaclassi hanno anche il __prepare__
metodo, che ti consente di valutare il corpo della classe in una mappatura diversa da a dict
, supportando così attributi ordinati, attributi sovraccaricati e altre cose fantastiche:
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...>
Si potrebbe sostenere che gli attributi ordinati possono essere ottenuti con i contatori di creazione e il sovraccarico può essere simulato con argomenti predefiniti:
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)
Oltre ad essere molto più brutto, è anche meno flessibile: e se volessi attributi letterali ordinati, come numeri interi e stringhe? E se None
è un valore valido per x
?
Ecco un modo creativo per risolvere il primo problema:
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']
Ed ecco un modo creativo per risolvere il secondo:
_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)
Ma questo è molto, MOLTO vudù di una semplice metaclasse (specialmente la prima, che ti scioglie davvero il cervello). Il punto è che tu consideri le metaclassi come poco familiari e controintuitive, ma puoi anche considerarle come il prossimo passo dell'evoluzione nei linguaggi di programmazione: devi solo aggiustare la tua mentalità. Dopotutto, potresti probabilmente fare tutto in C, inclusa la definizione di una struttura con puntatori a funzione e il suo passaggio come primo argomento alle sue funzioni. Una persona che vede C ++ per la prima volta potrebbe dire: "cos'è questa magia? Perché il compilatore passa implicitamentethis
ai metodi, ma non alle funzioni regolari e statiche? È meglio essere espliciti e verbosi sui propri argomenti ". Ma poi, la programmazione orientata agli oggetti è molto più potente una volta ottenuta; e anche questa, uh ... programmazione orientata agli aspetti, immagino. E una volta che tu capire le metaclassi, in realtà sono molto semplici, quindi perché non usarle quando è conveniente?
Infine, le metaclassi sono fantastiche e la programmazione dovrebbe essere divertente. L'utilizzo costante di costrutti di programmazione e modelli di progettazione standard è noioso e poco interessante e ostacola la tua immaginazione. Vivi un po! Ecco una metametaclass, solo per te.
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
modificare
Questa è una domanda piuttosto vecchia, ma sta ancora ricevendo voti positivi, quindi ho pensato di aggiungere un collegamento a una risposta più completa. Se desideri saperne di più sulle metaclassi e sul loro utilizzo, ho appena pubblicato un articolo a riguardo qui .