¿Cómo pasar argumentos a la metaclase de la definición de clase?

Estoy tratando de generar clases dinámicamente en Python 2.7, y me pregunto si puede pasar fácilmente argumentos a la metaclase desde el objeto de clase.

He leído esta publicación, que es increíble, pero no responde la pregunta. en el momento estoy haciendo:

def class_factory(args, to, meta_class): Class MyMetaClass(type): def __new__(cls, class_name, parents, attrs): attrs['args'] = args attrs['to'] = to attrs['eggs'] = meta_class class MyClass(object): metaclass = MyMetaClass ... 

pero esto me obliga a hacer lo siguiente

 MyClassClass = class_factory('spam', 'and', 'eggs') my_instance = MyClassClass() 

¿Hay una forma más limpia de hacer esto?

Sí, hay una manera fácil de hacerlo. En el método __new__() la metaclase, solo marque el diccionario de clase pasado como último argumento. Cualquier cosa definida en la statement de la class estará allí. Por ejemplo:

 class MyMetaClass(type): def __new__(cls, class_name, parents, attrs): if 'meta_args' in attrs: meta_args = attrs['meta_args'] attrs['args'] = meta_args[0] attrs['to'] = meta_args[1] attrs['eggs'] = meta_args[2] del attrs['meta_args'] # clean up return type.__new__(cls, class_name, parents, attrs) class MyClass(object): __metaclass__ = MyMetaClass meta_args = ['spam', 'and', 'eggs'] myobject = MyClass() from pprint import pprint pprint(dir(myobject)) print myobject.args, myobject.to, myobject.eggs 

Salida:

 ['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__metaclass__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'args', 'eggs', 'to'] spam and eggs 

Actualizar

El código anterior solo funcionará en Python 2 porque la syntax para especificar una metaclase se cambió de forma incompatible en Python 3.

Hacer que funcione en Python 3 (pero ya no en Python 2) es muy simple de hacer y solo requiere cambiar la definición de MyClass a:

 class MyClass(metaclass=MyMetaClass): meta_args = ['spam', 'and', 'eggs'] 

También es posible solucionar las diferencias de syntax y generar código que funcione tanto en Python 2 como en 3 creando las clases base “sobre la marcha” invocando explícitamente la metaclase y usando la clase que crea como base.

 class MyClass(MyMetaClass("NewBaseClass", (object,), {})): meta_args = ['spam', 'and', 'eggs'] 

La construcción de clases Python 3 también se ha modificado y se agregó soporte que permite otras formas de pasar argumentos, y en algunos casos, usarlos podría ser más fácil que la técnica que se muestra aquí. Todo depende de lo que estés tratando de lograr.

Consulte la respuesta detallada de @John Crawford para obtener una descripción del proceso en las nuevas versiones de Python.

Si bien la pregunta es para Python 2.7 y ya tiene una excelente respuesta, tuve la misma pregunta para Python 3.3 y este hilo fue lo más cercano a una respuesta que pude encontrar con Google. Encontré una mejor solución para Python 3.x buscando en la documentación de Python, y comparto mis hallazgos con cualquier persona que venga aquí en busca de una versión de Python 3.x.

Pasando argumentos a la metaclase en Python 3.x

Después de revisar la documentación oficial de Python, descubrí que Python 3.x ofrece un método nativo para pasar argumentos a la metaclase, aunque no sin sus defectos.

Simplemente agregue argumentos de palabras clave adicionales a su statement de clase:

 class C(metaclass=MyMetaClass, myArg1=1, myArg2=2): pass 

… y se pasan a tu metaclase así:

 class MyMetaClass(type): @classmethod def __prepare__(metacls, name, bases, **kargs): #kargs = {"myArg1": 1, "myArg2": 2} return super().__prepare__(name, bases, **kargs) def __new__(metacls, name, bases, namespace, **kargs): #kargs = {"myArg1": 1, "myArg2": 2} return super().__new__(metacls, name, bases, namespace) #DO NOT send "**kargs" to "type.__new__". It won't catch them and #you'll get a "TypeError: type() takes 1 or 3 arguments" exception. def __init__(cls, name, bases, namespace, myArg1=7, **kargs): #myArg1 = 1 #Included as an example of capturing metaclass args as positional args. #kargs = {"myArg2": 2} super().__init__(name, bases, namespace) #DO NOT send "**kargs" to "type.__init__" in Python 3.5 and older. You'll get a #"TypeError: type.__init__() takes no keyword arguments" exception. 

kargs dejar a kargs fuera de la llamada para type.__new__ y type.__init__ (Python 3.5 y type.__init__ anteriores; consulte “ACTUALIZAR”) o obtendrá una excepción TypeError debido a que se han pasado demasiados argumentos. Esto significa que, al pasar los argumentos de metaclase de esta manera, siempre tenemos que implementar MyMetaClass.__new__ y MyMetaClass.__init__ para evitar que nuestros argumentos de palabras clave personalizadas type.__init__ métodos de clase base type.__new__ y type.__init__ . type.__prepare__ parece manejar los argumentos de palabras clave adicionales con gracia (de ahí que los type.__prepare__ en el ejemplo, por si acaso hay alguna funcionalidad que no conozco que depende de **kargs ), por lo que definir el type.__prepare__ es opcional.

ACTUALIZAR

En Python 3.6, parece que el type se ajustó y el type.__init__ ahora puede manejar argumentos de palabras clave adicionales con gracia. Aún deberá definir el type.__new__ (lanza TypeError: __init_subclass__() takes no keyword arguments excepción de TypeError: __init_subclass__() takes no keyword arguments ).

Descompostura

En Python 3, especifica una metaclase a través del argumento de palabra clave en lugar del atributo de clase:

 class MyClass(metaclass=MyMetaClass): pass 

Esta statement se traduce aproximadamente a:

 MyClass = metaclass(name, bases, **kargs) 

… donde metaclass es el valor para el argumento de “metaclase” que pasaste, name es el nombre de la cadena de tu clase ( 'MyClass' ), bases son todas las clases base que pasaste (una tupla de longitud cero () en este caso), y kargs es cualquier argumento de palabra clave no capturado (un dict vacío {} en este caso).

Desglosando esto aún más, la statement se traduce aproximadamente a:

 namespace = metaclass.__prepare__(name, bases, **kargs) #`metaclass` passed implicitly since it's a class method. MyClass = metaclass.__new__(metaclass, name, bases, namespace, **kargs) metaclass.__init__(MyClass, name, bases, namespace, **kargs) 

… donde kargs es siempre el dict de los argumentos de palabras clave no capturados que pasamos a la definición de clase.

Rompiendo el ejemplo que he dado arriba:

 class C(metaclass=MyMetaClass, myArg1=1, myArg2=2): pass 

… aproximadamente se traduce en:

 namespace = MyMetaClass.__prepare__('C', (), myArg1=1, myArg2=2) #namespace={'__module__': '__main__', '__qualname__': 'C'} C = MyMetaClass.__new__(MyMetaClass, 'C', (), namespace, myArg1=1, myArg2=2) MyMetaClass.__init__(C, 'C', (), namespace, myArg1=1, myArg2=2) 

La mayor parte de esta información provino de la documentación de Python sobre “Personalización de la creación de clases” .