Recientemente me hicieron la misma pregunta y encontré varias respuestas. Espero que esté bien revivir este hilo, ya que quería desarrollar algunos de los casos de uso mencionados y agregar algunos nuevos.
La mayoría de las metaclases que he visto hacen una de dos cosas:
Registro (agregando una clase a una estructura de datos):
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
Siempre que subclases Model
, tu clase se registra en el models
diccionario:
>>> class A(Model):
... pass
...
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...>,
'B': <__main__.B class at 0x...>}
Esto también se puede hacer con los decoradores de clases:
models = {}
def model(cls):
models[cls.__name__] = cls
return cls
@model
class A(object):
pass
O con una función de registro explícita:
models = {}
def register_model(cls):
models[cls.__name__] = cls
class A(object):
pass
register_model(A)
En realidad, esto es más o menos lo mismo: mencionas a los decoradores de clases de manera desfavorable, pero en realidad no es más que azúcar sintáctico para la invocación de una función en una clase, por lo que no hay magia en ello.
De todos modos, la ventaja de las metaclases en este caso es la herencia, ya que funcionan para cualquier subclases, mientras que las otras soluciones solo funcionan para subclases explícitamente decoradas o registradas.
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...> # No B :(
Refactorización (modificando atributos de clase o agregando nuevos):
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
Siempre que subclases Model
y defines algunos Field
atributos, se inyectan con sus nombres (para mensajes de error más informativos, por ejemplo) y se agrupan en un _fields
diccionario (para una fácil iteración, sin tener que revisar todos los atributos de clase y todas sus clases base ' atributos cada vez):
>>> class A(Model):
... foo = Integer()
...
>>> class B(A):
... bar = String()
...
>>> B._fields
{'foo': Integer('A.foo'), 'bar': String('B.bar')}
Nuevamente, esto se puede hacer (sin herencia) con un decorador de clases:
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 explícitamente:
class A(object):
foo = Integer('A.foo')
_fields = {'foo': foo} # Don't forget all the base classes' fields, too!
Aunque, al contrario de su defensa de la programación no metabólica legible y mantenible, esto es mucho más engorroso, redundante y propenso a errores:
class B(A):
bar = String()
# vs.
class B(A):
bar = String('bar')
_fields = {'B.bar': bar, 'A.foo': A.foo}
Habiendo considerado los casos de uso más comunes y concretos, los únicos casos en los que absolutamente TIENE que usar metaclases son cuando desea modificar el nombre de la clase o la lista de clases base, porque una vez definidos, estos parámetros se incorporan a la clase y no al decorador. o la función puede deshacerlos.
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
Esto puede ser útil en marcos para emitir advertencias siempre que se definan clases con nombres similares o árboles de herencia incompletos, pero no puedo pensar en una razón además de trollear para cambiar estos valores. Quizás David Beazley pueda.
De todos modos, en Python 3, las metaclases también tienen el __prepare__
método, que le permite evaluar el cuerpo de la clase en un mapeo que no sea a dict
, por lo que admite atributos ordenados, atributos sobrecargados y otras cosas interesantes y perversas:
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...>
Puede argumentar que los atributos ordenados se pueden lograr con contadores de creación y la sobrecarga se puede simular con argumentos predeterminados:
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)
Además de ser mucho más feo, también es menos flexible: ¿qué pasa si quieres atributos literales ordenados, como enteros y cadenas? ¿Y si None
es un valor válido para x
?
He aquí una forma creativa de resolver el primer 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']
Y aquí hay una forma creativa de resolver el segundo:
_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)
Pero esto es mucho, MUCHO vudú que una simple metaclase (especialmente la primera, que realmente derrite tu cerebro). Mi punto es que usted ve las metaclases como algo desconocido y contrario a la intuición, pero también puede verlas como el siguiente paso de evolución en los lenguajes de programación: solo tiene que ajustar su forma de pensar. Después de todo, probablemente podría hacer todo en C, incluida la definición de una estructura con punteros de función y pasarla como primer argumento a sus funciones. Una persona que vea C ++ por primera vez podría decir: "¿Qué es esta magia? ¿Por qué el compilador pasa implícitamentethis
a los métodos, pero no a las funciones regulares y estáticas? Es mejor ser explícito y detallado sobre tus argumentos ". Pero entonces, la programación orientada a objetos es mucho más poderosa una vez que la obtienes; y también lo es, eh ... la programación casi orientada a aspectos, supongo. Y una vez que entender las metaclases, en realidad son muy simples, así que ¿por qué no usarlas cuando sea conveniente?
Y, finalmente, las metaclases son fantásticas y la programación debería ser divertida. Usar construcciones de programación estándar y patrones de diseño todo el tiempo es aburrido y poco inspirador, y obstaculiza su imaginación. ¡Vive un poco! Aquí tienes una metametaclase, solo para ti.
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
Editar
Esta es una pregunta bastante antigua, pero todavía está recibiendo votos positivos, así que pensé en agregar un enlace a una respuesta más completa. Si desea leer más sobre las metaclases y sus usos, acabo de publicar un artículo al respecto aquí .