¿Cuáles son algunos casos de uso (concretos) para metaclases?

Tengo un amigo al que le gusta usar metaclases y regularmente los ofrece como una solución.

Soy de la mente que casi nunca necesitas usar metaclases. ¿Por qué? porque me imagino que si estás haciendo algo así a una clase, probablemente deberías estar haciéndolo a un objeto. Y un pequeño rediseño / refactor está en orden.

Ser capaz de usar metaclases ha causado que muchas personas en muchos lugares usen las clases como un tipo de objeto de segunda clase, lo que me parece desastroso. ¿La progtwigción será reemplazada por la meta-progtwigción? La adición de decoradores de clase desafortunadamente lo ha hecho aún más aceptable.

Así que, por favor, estoy desesperado por conocer sus casos de uso válidos (concretos) para metaclases en Python. O para saber por qué mutar clases es mejor que mutar objetos, a veces.

Comenzaré:

A veces, cuando se utiliza una biblioteca de terceros, es útil poder mutar la clase de una manera determinada.

(Este es el único caso que se me ocurre, y no es concreto)

Tengo una clase que maneja trazados no interactivos, como una interfaz para Matplotlib. Sin embargo, en ocasiones uno quiere hacer una ttwig interactiva. Con solo un par de funciones encontré que pude incrementar el conteo de figuras, dibujar llamadas manualmente, etc., pero necesitaba hacer esto antes y después de cada llamada de trazado. Así que para crear tanto un envoltorio de trazado interactivo como un envoltorio de trazado fuera de la pantalla, encontré que era más eficiente hacerlo a través de metaclases, envolviendo los métodos apropiados, que hacer algo como:

class PlottingInteractive: add_slice = wrap_pylab_newplot(add_slice) 

Este método no se mantiene actualizado con los cambios de API, etc., pero uno que itera sobre los atributos de clase en __init__ antes de volver a establecer los atributos de clase es más eficiente y mantiene las cosas actualizadas:

 class _Interactify(type): def __init__(cls, name, bases, d): super(_Interactify, cls).__init__(name, bases, d) for base in bases: for attrname in dir(base): if attrname in d: continue # If overridden, don't reset attr = getattr(cls, attrname) if type(attr) == types.MethodType: if attrname.startswith("add_"): setattr(cls, attrname, wrap_pylab_newplot(attr)) elif attrname.startswith("set_"): setattr(cls, attrname, wrap_pylab_show(attr)) 

Por supuesto, podría haber mejores maneras de hacer esto, pero he encontrado que esto es efectivo. Por supuesto, esto también podría hacerse en __new__ o __init__ , pero esta fue la solución que encontré más sencilla.

Recientemente me hicieron la misma pregunta y se me ocurrieron 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:

  1. 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 subclase Model , su clase se registra en el diccionario de models :

     >>> 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 decoradores de clase:

     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 casi lo mismo: usted menciona a los decoradores de la clase de manera desfavorable, pero en realidad no es más que azúcar sintáctico para una invocación de función en una clase, por lo que no hay magia al respecto.

    De todos modos, la ventaja de las metaclases en este caso es la herencia, ya que funcionan para cualquier subclase, 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 :( 
  2. Refactorización (modificando atributos de clase o añadiendo 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 

    Cada vez que subclasifique Model y defina algunos atributos de Field , se les inyectan sus nombres (para mensajes de error más informativos, por ejemplo), y se agrupan en un diccionario de _fields (para una fácil iteración, sin tener que revisar todos los atributos de clase y todos sus atributos). atributos de las clases base 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 clase:

     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 una progtwigción no meta 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 TENGO QUE usar metaclases absolutamente cuando se 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 son decoradores. O la función puede desbloquearlos.

 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 aparte de la función de arrastre para cambiar realmente estos valores. Tal vez David Beazley pueda.

De todos modos, en Python 3, las metaclases también tienen el método __prepare__ , que te permite evaluar el cuerpo de la clase en una asignación que no sea un dict , por lo que admite atributos ordenados, atributos sobrecargados y otras cosas geniales:

 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 [, ] rather than  

Podría argumentar que los atributos ordenados se pueden lograr con los 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é sucede si desea atributos literales ordenados, como enteros y cadenas? ¿Qué pasa si None es un valor válido para x ?

Aquí hay una manera 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ú-er que una simple metaclase (especialmente la primera, que realmente derrite tu cerebro). Lo que quiero decir es que considera que las metaclases no son familiares y no son intuitivas, pero también puede considerarlas como el siguiente paso de la evolución en los lenguajes de progtwigción: solo tiene que ajustar su mentalidad. 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 el primer argumento de sus funciones. Una persona que ve C ++ por primera vez podría decir: “¿qué es esta magia? ¿Por qué el comstackdor pasa this implícitamente a los métodos, pero no a las funciones regulares y estáticas? Es mejor ser explícito y detallado acerca de sus argumentos”. Pero entonces, la progtwigción orientada a objetos es mucho más poderosa una vez que la obtienes; y así es, uh … la progtwigción orientada a casi aspectos, supongo. Y una vez que entiendes las metaclases, en realidad son muy simples, ¿por qué no usarlas cuando sea conveniente?

Y finalmente, las metaclases son rad, y la progtwigción debería ser divertida. Usar construcciones de progtwigción estándar y patrones de diseño todo el tiempo es aburrido y poco inspirador, y dificulta su imaginación. ¡Vive un poco! Aquí hay 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 

El propósito de las metaclases no es reemplazar la distinción clase / objeto con metaclase / clase, es cambiar el comportamiento de las definiciones de clase (y, por lo tanto, sus instancias) de alguna manera. Efectivamente, es alterar el comportamiento de la statement de la clase de manera que pueda ser más útil para su dominio particular que el predeterminado. Las cosas para las que las he usado son:

  • Seguimiento de subclases, generalmente para registrar manejadores. Esto es útil cuando se utiliza una configuración de estilo de complemento, donde desea registrar un controlador para una cosa en particular simplemente mediante la subclasificación y la configuración de algunos atributos de clase. p.ej. Supongamos que escribe un controlador para varios formatos de música, donde cada clase implementa métodos apropiados (jugar / obtener tags, etc.) para su tipo. Agregar un controlador para un nuevo tipo se convierte en:

     class Mp3File(MusicFile): extensions = ['.mp3'] # Register this type as a handler for mp3 files ... # Implementation of mp3 methods go here 

    La metaclase mantiene un diccionario de {'.mp3' : MP3File, ... } etc, y construye un objeto del tipo apropiado cuando solicita un manejador a través de una función de fábrica.

  • Cambio de comportamiento. Es posible que desee adjuntar un significado especial a ciertos atributos, lo que resulta en un comportamiento alterado cuando están presentes. Por ejemplo, puede buscar métodos con el nombre _get_foo y _set_foo y convertirlos de forma transparente en propiedades. Como ejemplo del mundo real, aquí hay una receta que escribí para dar más definiciones de estructura tipo C. La metaclase se usa para convertir los elementos declarados en una cadena de formato de estructura, manejo de herencia, etc., y producir una clase capaz de lidiar con ella.

    Para otros ejemplos del mundo real, eche un vistazo a varios ORM, como ORM de sqlalchemy o sqlobject . Nuevamente, el propósito es interpretar definiciones (aquí definiciones de columna SQL) con un significado particular.

Empecemos con la cita clásica de Tim Peter:

Las metaclases son una magia más profunda de lo que el 99% de los usuarios debería preocuparse. Si se pregunta si los necesita, no lo hace (las personas que realmente los necesitan saben con certeza que los necesitan y no necesitan una explicación de por qué). Tim Peters (clp post 2002-12-22)

Dicho esto, (periódicamente) he encontrado usos reales de metaclases. El que me viene a la mente es en Django, donde todos sus modelos heredan de los modelos. Modelo. Modelos. El modelo, a su vez, hace un poco de magia seria para envolver sus modelos DB con la bondad ORM de Django. Esa magia pasa por metaclases. Crea todo tipo de clases de excepción, clases de administrador, etc. etc.

Vea django / db / models / base.py, clase ModelBase () para el comienzo de la historia.

Las metaclases pueden ser útiles para la construcción de lenguajes específicos de dominio en Python. Ejemplos concretos son Django, la syntax declarativa de SQLObject de los esquemas de base de datos.

Un ejemplo básico de una metaclase conservadora por Ian Bicking:

Las metaclases que he usado han sido principalmente para apoyar un tipo de estilo declarativo de progtwigción. Por ejemplo, considere un esquema de validación:

 class Registration(schema.Schema): first_name = validators.String(notEmpty=True) last_name = validators.String(notEmpty=True) mi = validators.MaxLength(1) class Numbers(foreach.ForEach): class Number(schema.Schema): type = validators.OneOf(['home', 'work']) phone_number = validators.PhoneNumber() 

Algunas otras técnicas: Ingredientes para crear un DSL en Python (pdf).

Edición (por Ali): un ejemplo de cómo hacerlo utilizando colecciones e instancias es lo que preferiría. El hecho importante son las instancias, que le dan más poder y eliminan las razones para usar las metaclases. Además, vale la pena señalar que su ejemplo utiliza una combinación de clases e instancias, lo que seguramente es una indicación de que no puede hacerlo todo con metaclases. Y crea una manera verdaderamente no uniforme de hacerlo.

 number_validator = [ v.OneOf('type', ['home', 'work']), v.PhoneNumber('phone_number'), ] validators = [ v.String('first_name', notEmpty=True), v.String('last_name', notEmpty=True), v.MaxLength('mi', 1), v.ForEach([number_validator,]) ] 

No es perfecto, pero ya casi no hay magia, no se necesitan metaclases y se ha mejorado la uniformidad.

Un patrón razonable de uso de metaclase es hacer algo una vez cuando se define una clase en lugar de repetidamente cada vez que se crea una instancia de la misma clase.

Cuando varias clases comparten el mismo comportamiento especial, repetir __metaclass__=X es obviamente mejor que repetir el código de propósito especial y / o introducir superclases compartidas ad-hoc.

Pero incluso con una sola clase especial y sin extensión previsible, __new__ y __init__ de una metaclase son una forma más limpia de inicializar variables de clase u otros datos globales que mezclar código de propósito especial y declaraciones de def y class normales en el cuerpo de definición de clase.

La única vez que usé metaclases en Python fue cuando escribía un contenedor para la API de Flickr.

Mi objective era raspar el sitio de API de Flickr y generar dinámicamente una jerarquía de clases completa para permitir el acceso a la API utilizando objetos de Python:

 # Both the photo type and the flickr.photos.search API method # are generated at "run-time" for photo in flickr.photos.search(text=balloons): print photo.description 

Así que en ese ejemplo, como generé la API de Flickr de Python completa desde el sitio web, realmente no conozco las definiciones de clase en tiempo de ejecución. Ser capaz de generar dinámicamente tipos fue muy útil.

Estaba pensando lo mismo justo ayer y completamente de acuerdo. Las complicaciones en el código causadas por los bashs de hacerlo más declarativo generalmente hacen que el código base sea más difícil de mantener, más difícil de leer y menos python en mi opinión. Normalmente, también requiere una gran cantidad de copy.copy () ing (para mantener la herencia y copiar desde la clase a la instancia) y significa que tiene que buscar en muchos lugares para ver qué está pasando (siempre mirando desde la metaclase hacia arriba) que va en contra de la grano de python también. He estado seleccionando código de formencode y sqlalchemy para ver si un estilo tan declarativo valía la pena y claramente no. Dicho estilo debe dejarse para los descriptores (como propiedades y métodos) y datos inmutables. Ruby tiene un mejor soporte para estos estilos declarativos y me alegro de que el lenguaje Python principal no vaya por ese camino.

Puedo ver su uso para la depuración, agregar una metaclase a todas sus clases base para obtener información más completa. También veo su uso solo en proyectos (muy) grandes para deshacerse de algún código repetitivo (pero a falta de claridad). sqlalchemy, por ejemplo , los usa en otro lugar, para agregar un método personalizado particular a todas las subclases en función de un valor de atributo en su definición de clase, por ejemplo, un ejemplo de juguete

 class test(baseclass_with_metaclass): method_maker_value = "hello" 

podría tener una metaclase que generara un método en esa clase con propiedades especiales basadas en “hola” (diga un método que agregue “hola” al final de una cadena). Podría ser bueno para la capacidad de mantenimiento asegurarse de que no tenía que escribir un método en cada subclase que haga, en su lugar, todo lo que tiene que definir es method_maker_value.

Sin embargo, la necesidad de esto es muy rara y solo reduce un poco la escritura, por lo que no vale la pena considerarlo a menos que tenga una base de código lo suficientemente grande.

Nunca es absolutamente necesario usar una metaclase, ya que siempre puede construir una clase que haga lo que quiera usando la herencia o la agregación de la clase que desea modificar.

Dicho esto, puede ser muy útil en Smalltalk y Ruby para poder modificar una clase existente, pero a Python no le gusta hacerlo directamente.

Hay un excelente artículo de DeveloperWorks sobre metaclases en Python que podría ayudar. El artículo de Wikipedia también es bastante bueno.

¡Las metaclasas no están reemplazando la progtwigción! Son solo un truco que puede automatizar o hacer más elegantes algunas tareas. Un buen ejemplo de esto es la biblioteca de resaltado de syntax de Pygments . Tiene una clase llamada RegexLexer que le permite al usuario definir un conjunto de reglas de lexing como expresiones regulares en una clase. Se utiliza una metaclase para convertir las definiciones en un analizador útil.

Son como la sal; Es fácil de usar demasiado.

La forma en que usé metaclases fue proporcionar algunos atributos a las clases. Tomar como ejemplo:

 class NameClass(type): def __init__(cls, *args, **kwargs): type.__init__(cls, *args, **kwargs) cls.name = cls.__name__ 

pondrá el atributo de nombre en cada clase que tendrá la metaclase establecida para que apunte a NameClass.

Algunas bibliotecas GUI tienen problemas cuando varios subprocesos intentan interactuar con ellos. tkinter es uno de esos ejemplos; y mientras que uno puede manejar explícitamente el problema con eventos y colas, puede ser mucho más sencillo usar la biblioteca de una manera que ignore el problema por completo. He aquí la magia de las metaclases.

Ser capaz de reescribir dinámicamente una biblioteca completa sin problemas para que funcione correctamente como se espera en una aplicación multiproceso puede ser extremadamente útil en algunas circunstancias. El módulo safetkinter hace eso con la ayuda de una metaclase proporcionada por el módulo de threadbox : no se necesitan eventos ni colas.

Un aspecto threadbox de threadbox es que no le importa qué clase clone. Proporciona un ejemplo de cómo todas las clases base pueden ser tocadas por una metaclase si es necesario. Un beneficio adicional que viene con las metaclases es que también se ejecutan en clases heredadas. Progtwigs que se escriben solos, ¿por qué no?

El único caso de uso legítimo de una metaclase es evitar que otros desarrolladores entrometidos toquen su código. Una vez que un desarrollador entrometido domina las metaclases y comienza a hurgar con el tuyo, lanza otro nivel o dos para mantenerlos fuera. Si eso no funciona, comience a usar el type.__new__ o quizás algún esquema usando una metaclase recursiva.

(escrito lengua en la mejilla, pero he visto este tipo de ofuscación hecha. Django es un ejemplo perfecto)

Este es un uso menor, pero … una cosa para la que he encontrado metaclases es para invocar una función cada vez que se crea una subclase. Codifiqué esto en una metaclase que busca un atributo __initsubclass__ : cada vez que se crea una subclase, todas las clases primarias que definen ese método se invocan con __initsubclass__(cls, subcls) . Esto permite la creación de una clase padre que luego registra todas las subclases con algún registro global, ejecuta comprobaciones invariables en las subclases cuando se definen, realiza operaciones de enlace tardío, etc. Sin necesidad de llamar manualmente a funciones o crear metaclases personalizadas que realizar cada uno de estos deberes separados.

Tenga en cuenta que poco a poco me doy cuenta de que la magia implícita de este comportamiento es algo indeseable, ya que es inesperado si se ve fuera de contexto una definición de clase … y por eso me he alejado de usar esa solución para cualquier otra cosa que no sea seria inicializando un atributo __super para cada clase e instancia.

Hace poco tuve que usar una metaclase para ayudar a definir de forma declarativa un modelo SQLAlchemy en torno a una tabla de base de datos con datos del Censo de EE. UU. De http://census.ire.org/data/bulkdata.html

IRE proporciona shells de base de datos para las tablas de datos del censo, que crean columnas enteras siguiendo una convención de nomenclatura de la Oficina del Censo de p012015, p012016, p012017, etc.

Quería a) poder acceder a estas columnas usando una syntax de model_instance.p012017 , b) ser bastante explícito sobre lo que estaba haciendo yc) no tener que definir explícitamente docenas de campos en el modelo, por lo que subclasificé DeclarativeMeta de SQLAlchemy para iterar a través de un rango de columnas y crear automáticamente campos de modelo correspondientes a las columnas:

 from sqlalchemy.ext.declarative.api import DeclarativeMeta class CensusTableMeta(DeclarativeMeta): def __init__(cls, classname, bases, dict_): table = 'p012' for i in range(1, 49): fname = "%s%03d" % (table, i) dict_[fname] = Column(Integer) setattr(cls, fname, dict_[fname]) super(CensusTableMeta, cls).__init__(classname, bases, dict_) 

Luego podría usar esta metaclase para mi definición de modelo y acceder a los campos enumerados automáticamente en el modelo:

 CensusTableBase = declarative_base(metaclass=CensusTableMeta) class P12Tract(CensusTableBase): __tablename__ = 'ire_p12' geoid = Column(String(12), primary_key=True) @property def male_under_5(self): return self.p012003 ... 

Parece que hay un uso legítimo descrito aquí : reescritura de cadenas de caracteres de Python con una metaclase.

Tuve que usarlos una vez para un analizador binario para que sea más fácil de usar. Usted define una clase de mensaje con atributos de los campos presentes en el cable. Necesitaban ser ordenados de la manera en que fueron declarados para construir el formato de cable final. Puede hacerlo con metaclases, si usa un orden de espacio de nombres ordenado. De hecho, está en los ejemplos para Metaclasses:

https://docs.python.org/3/reference/datamodel.html#metaclass-example

Pero en general: evalúe con mucho cuidado, si realmente necesita la complejidad agregada de las metaclases.