¿Cuáles son sus casos de uso(concretos) para las metaclases en Python?
metaclass (17)
¡Las metaclases no están reemplazando la programació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 sintaxis 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 usa una metaclase para convertir las definiciones en un analizador útil.
Son como la sal; es fácil de usar demasiado.
Tengo un amigo al que le gusta usar metaclases y regularmente los ofrece como una solución.
Estoy de acuerdo con que casi nunca necesitas usar metaclases. ¿Por qué? porque me imagino que si haces algo así para una clase, probablemente deberías estar haciéndolo a un objeto. Y un pequeño rediseño / refactor está en orden.
Poder usar metaclases ha hecho que mucha gente en muchos lugares use clases como un tipo de objeto de segunda clase, lo que me parece desastroso. ¿La programación debe ser reemplazada por metaprogramación? La adición de decoradores de clase ha hecho, lamentablemente, aún más aceptable.
Entonces, por favor, estoy desesperado por conocer sus casos de uso válidos (concretos) para las metaclases en Python. O estar iluminado sobre por qué mutar las clases es mejor que mutar los objetos, a veces.
Comenzaré:
A veces, cuando se utiliza una biblioteca de terceros, es útil poder modificar la clase de cierta manera.
(Este es el único caso en el que puedo pensar, y no es concreto)
Algunas bibliotecas de GUI tienen problemas cuando varios hilos intentan interactuar con ellos. tkinter
es uno de esos ejemplos; y aunque uno puede manejar explícitamente el problema con eventos y colas, puede ser mucho más simple 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 con la ayuda de una metaclase proporcionada por el módulo de threadbox : eventos y colas no necesarios.
Un aspecto threadbox
de la 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. Otro beneficio adicional de las metaclases es que también se ejecutan en clases heredadas. Programas que se escriben a sí mismos, ¿por qué no?
Comencemos con la frase clásica de Tim Peter:
Las metaclases son una magia más profunda de la que el 99% de los usuarios debería preocuparse. Si se pregunta si los necesita, no (las personas que realmente los necesitan saben con certeza que los necesitan y no necesitan una explicación sobre por qué). Tim Peters (clp post 2002-12-22)
Habiendo dicho eso, me he topado (periódicamente) con los verdaderos usos de las metaclases. Lo que viene a la mente es en Django, donde todos sus modelos heredan de los modelos. Modelo. models.Model, a su vez, hace algo de magia para envolver sus modelos DB con la bondad ORM de Django. Esa magia sucede a través de metaclases. Crea todo tipo de clases de excepciones, clases de administrador, etc., etc.
Consulte django / db / models / base.py, clase ModelBase () para el comienzo de la historia.
El único caso de uso legítimo de una metaclass es evitar que otros desarrolladores curiosos toquen tu código. Una vez que un desarrollador entrometido domina las metaclases y comienza a hurgar con las suyas, agregue otro nivel o dos para mantenerlas fuera. Si eso no funciona, comience a usar type.__new__
o tal vez algún esquema utilizando una metaclass recursiva.
(Tengo una sonrisa en la mejilla, pero he visto este tipo de ofuscación hecha. Django es un ejemplo perfecto)
El objetivo 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 para alterar el comportamiento de la declaración de clase en formas que pueden ser más útiles para su dominio particular que el predeterminado. Las cosas que los he usado son:
Seguimiento de subclases, generalmente para registrar manejadores. Esto es útil cuando se usa una configuración de estilo de complemento, donde se desea registrar un manejador para una cosa particular simplemente subclasificando y configurando algunos atributos de clase. p.ej. supongamos que escribe un controlador para varios formatos de música, donde cada clase implementa los métodos apropiados (reproducir / obtener etiquetas, 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 entonces mantiene un diccionario de
{''.mp3'' : MP3File, ... }
etc, y construye un objeto del tipo apropiado cuando solicita un controlador a través de una función de fábrica.Cambiando el comportamiento. Es posible que desee agregar un significado especial a ciertos atributos, lo que resulta en un comportamiento alterado cuando están presentes. Por ejemplo, es posible que desee buscar métodos con el nombre
_get_foo
y_set_foo
y convertirlos de forma transparente en propiedades. Como un ejemplo del mundo real, here''s 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, manejar la herencia, etc., y generar una clase capaz de manejarla.Para otros ejemplos del mundo real, eche un vistazo a varios ORM, como sqlalchemy''s ORM o sqlobject . Nuevamente, el propósito es interpretar definiciones (aquí definiciones de columna SQL) con un significado particular.
Estaba pensando lo mismo ayer y estoy completamente de acuerdo. Las complicaciones en el código causadas por los intentos de hacerlo más declarativo generalmente hacen que la base de código sea más difícil de mantener, más difícil de leer y menos pitónica, en mi opinión. También normalmente requiere una gran cantidad de copy.copy () ing (para mantener la herencia y copiar de una clase a otra) y significa que debe buscar en muchos lugares para ver qué está sucediendo (siempre mirando hacia arriba desde la metaclase) lo cual va en contra del grano de pitón también. He estado seleccionando formencode y sqlalchemy code para ver si ese estilo declarativo valió la pena y es evidente que no. Tal estilo debe dejarse a los descriptores (como propiedad y métodos) y datos inmutables. Ruby tiene mejor soporte para tales estilos declarativos y me alegro de que el lenguaje Python central no vaya por esa ruta.
Puedo ver su uso para la depuración, agregue 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 pérdida de claridad). sqlalchemy, por example , 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" (digamos un método que agregó "hola" al final de una cadena). Podría ser bueno para la mantenibilidad para asegurarse de que no tiene que escribir un método en cada subclase que haga; en su lugar, todo lo que tiene que definir es method_maker_value.
La necesidad de esto es muy rara y solo reduce un poco el tipeo que no merece la pena considerar a menos que tengas una base de código lo suficientemente grande.
Este es un uso menor, pero ... una cosa que he encontrado útil para las metaclases es invocar una función cada vez que se crea una subclase. Codifiqué esto en una metaclase que busca un atributo __initsubclass__
: siempre que se crea una subclase, todas las clases padre que definen ese método se invocan con __initsubclass__(cls, subcls)
. Esto permite la creación de una clase principal que luego registra todas las subclases con algún registro global, ejecuta comprobaciones invariables en subclases cada vez que se definen, realiza operaciones de enlace tardío, etc. ... todo sin tener que llamar funciones manualmente o crear metaclases personalizadas que realizar cada uno de estos deberes separados.
Eso sí, poco a poco me he dado cuenta de que la magia implícita de este comportamiento es algo indeseable, ya que es inesperado si miras la definición de clase fuera de contexto ... y por eso me he alejado de esa solución para cualquier cosa seria además inicializando un atributo __super
para cada clase e instancia.
Hace poco tuve que usar una metaclase para ayudar a definir declarativamente un modelo de SQLAlchemy en torno a una tabla de base de datos poblada con datos del Censo de EE. UU. Desde 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 sintaxis 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, así que subclasifedé el DeclarativeMeta
de SQLAlchemy para iterar a través de un rango de columnas y automáticamente crea campos 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_)
Podría usar esta metaclase para la definición de mi 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
...
La única vez que usé metaclases en Python fue cuando escribía un contenedor para la API de Flickr.
Mi objetivo era eliminar el sitio api de flickr y generar dinámicamente una jerarquía de clases completa para permitir el acceso a la API usando 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
Entonces, en ese ejemplo, porque genere toda la API de Python Flickr desde el sitio web, realmente no conozco las definiciones de clase en tiempo de ejecución. Ser capaz de generar tipos dinámicamente fue muy útil.
La forma en que usé las metaclases fue para proporcionar algunos atributos a las clases. Toma por 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 configurada para señalar a NameClass.
Las metaclases pueden ser útiles para la construcción de lenguajes específicos de dominio en Python. Los ejemplos concretos son Django, la sintaxis declarativa de los esquemas de la base de datos de SQLObject.
Un ejemplo básico de A Conservative Metaclass por Ian Bicking:
Las metaclases que he usado han sido principalmente para apoyar una especie de estilo declarativo de programació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 construir un DSL en Python (pdf).
Editar (por Ali): un ejemplo de hacer esto usando colecciones e instancias es lo que preferiría. El hecho importante son las instancias, que le dan más poder y eliminan la razón para usar metaclases. Además vale la pena señalar que su ejemplo utiliza una combinación de clases e instancias, lo que sin duda es una indicación de que no puede simplemente hacer todo con metaclases. Y crea una forma 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 hay necesidad de metaclases y una uniformidad mejorada.
Nunca es absolutamente necesario usar una metaclase, ya que siempre puede construir una clase que haga lo que quiera utilizando 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 hacer eso directamente.
Existe un excelente artículo de DeveloperWorks sobre metaclasificación en Python que podría ser útil. El artículo de Wikipedia también es bastante bueno.
Parece que hay un uso legítimo que se describe here : Reescribir las Docstrings de Python con una Metaclass.
Recientemente me hicieron la misma pregunta y obtuve varias respuestas. Espero que esté bien revivir este hilo, ya que quería detallar 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 subclass
Model
, su clase está registrada en el diccionario demodels
:>>> 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 más o menos lo mismo: mencionas desfavorablemente a los decoradores de clase, pero en realidad no es nada más que azúcar sintáctica para invocar una función en una clase, así que no hay ninguna 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 :(
Refactorización (modificación de los atributos de clase o adición de 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 subclases un
Model
y defines algunos atributos deField
, se les inyectan sus nombres (para más mensajes de error informativos, por ejemplo) y se agrupan en un diccionario_fields
(para facilitar la iteración, sin tener que examinar todos los atributos de clase y todo su atributos de las clases base todo el tiempo):>>> 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 la programació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 donde TENGA que usar metaclases son cuando desea modificar el nombre de clase o la lista de clases base, porque una vez definidos, estos parámetros se incorporan a la clase y no se decora 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 los 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 del enrutamiento para cambiar realmente estos valores. Quizás David Beazley pueda.
De todos modos, en Python 3, las metaclases también tienen el método __prepare__
, que le permite evaluar el cuerpo de la clase en una asignación que no sea un dict
, apoyando así atributos ordenados, atributos sobrecargados y otras cosas geniales malvadas:
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...>
Podría 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: ¿y si quieres atributos literales ordenados, como enteros y cadenas? ¿Qué pasa si None
es un valor válido para x
?
Aquí hay 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 más vudú que una metaclass simple (especialmente la primera, que realmente derrite tu cerebro). Mi punto es que miras las metaclases como desconocidas y contra-intuitivas, pero también puedes verlas como el próximo paso de la evolución en los lenguajes de programación: solo tienes que ajustar tu mentalidad. Después de todo, probablemente puedas hacer todo en C, incluida la definición de una estructura con punteros a funciones y pasarla como primer argumento para sus funciones. Una persona que vea C ++ por primera vez podría decir: "¿qué es esta magia? ¿Por qué el compilador pasa implícitamente this
a los métodos, pero no a las funciones regulares y estáticas? Es mejor ser explícito y detallado sobre sus argumentos". Pero luego, la programación orientada a objetos es mucho más poderosa una vez que la obtiene; y así es esto, eh ... la programación orientada a cuasi-aspectos, supongo. Y una vez que comprenda las metaclases, en realidad son muy simples, ¿por qué no utilizarlas cuando sea conveniente?
Y, por último, las metaclases son rad, y la programación debe ser divertida. El uso de 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í 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
Tengo una clase que maneja el trazado no interactivo, como una interfaz para Matplotlib. Sin embargo, en ocasiones uno quiere hacer un trazado interactivo. Con solo un par de funciones descubrí que podía aumentar el recuento de las figuras, dibujar llamadas manualmente, etc., pero tenía que hacer esto antes y después de cada llamada de trazado. Entonces, para crear tanto un contenedor de trazado interactivo como un contenedor de trazado fuera de pantalla, encontré que era más eficiente hacerlo a través de metaclases, ajustando los métodos apropiados, que hacer algo como:
class PlottingInteractive:
add_slice = wrap_pylab_newplot(add_slice)
Este método no se mantiene al día con los cambios de la API y demás, 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 al día:
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 formas de hacerlo, 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 directa.
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 los atributos de los campos presentes en el cable. Necesitaban ordenarse de la forma en que fueron declarados para construir el formato de cable final a partir de él. Puede hacer eso con metaclases, si usa un diccionario de nombres ordenado. De hecho, está en los ejemplos de Metaclasses:
https://docs.python.org/3/reference/datamodel.html#metaclass-example
Pero en general: evalúe con mucho cuidado si realmente necesita la complejidad adicional de las metaclases.
Un patrón razonable de uso de metaclass hace 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 solo una clase especial y ninguna 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 entremezclar código de propósito especial y declaraciones de def
y class
normales en el cuerpo de definición de clase.