¿Cómo pasar argumentos a la metaclase de la definición de clase en Python 3.x?

¿Esta es una versión de Python 3.x de Cómo pasar argumentos a la metaclase de la definición de clase? pregunta, enumerada por separado por solicitud ya que la respuesta es significativamente diferente de Python 2.x.


En Python 3.x, ¿cómo paso los argumentos a las __prepare__ , __new__ y __init__ una metaclase para que un autor de la clase pueda aportar información a la metaclase sobre cómo se debe crear la clase?

Como mi caso de uso, estoy usando metaclases para habilitar el registro automático de clases y sus subclases en PyYAML para cargar / guardar archivos YAML. Esto implica alguna lógica de tiempo de ejecución adicional no disponible en el stock YAMLObjectMetaClass . Además, quiero permitir que los autores de la clase especifiquen opcionalmente la etiqueta / plantilla de formato de etiqueta que PyYAML utiliza para representar la clase y / o la función que los objetos deben usar para la construcción y representación. Ya me he dado cuenta de que no puedo usar una subclase de YAMLObjectMetaClass de YAMLObjectMetaClass para lograr esto, “porque no tenemos acceso al objeto de clase real en __new__ ” según mi comentario de código, por lo que estoy escribiendo Mi propia metaclase que envuelve las funciones de registro de PyYAML.

En última instancia, quiero hacer algo como:

 from myutil import MyYAMLObjectMetaClass class MyClass(metaclass=MyYAMLObjectMetaClass): __metaclassArgs__ = () __metaclassKargs__ = {"tag": "!MyClass"} 

… donde __metaclassArgs__ y __metaclassKargs__ serían argumentos que irán a los __prepare__ , __new__ y __init__ de MyYAMLObjectMetaClass cuando se MyYAMLObjectMetaClass el objeto de clase MyClass .

Por supuesto, podría usar el enfoque de “nombres de atributos reservados” que figura en la versión de Python 2.x de esta pregunta , pero sé que hay un enfoque más elegante disponible.

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” a continuación) 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” .

Aquí hay una versión del código de mi respuesta a esa otra pregunta sobre los argumentos de metaclase que se ha actualizado para que funcione tanto en Python 2 como en 3. En esencia, hace lo mismo que la función with_metaclass() los seis módulos de Benjamin Peterson: que consiste en crear explícitamente una nueva clase base utilizando la metaclase deseada sobre la marcha, siempre que sea necesario y evitando así errores debido a las diferencias de syntax de metaclase entre las dos versiones de Python (porque la forma de hacerlo no cambió) .

 from __future__ import print_function from pprint import pprint 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) # Creates base class on-the-fly using syntax which is valid in both # Python 2 and 3. class MyClass(MyMetaClass("NewBaseClass", (object,), {})): meta_args = ['spam', 'and', 'eggs'] myobject = MyClass() pprint(vars(MyClass)) print(myobject.args, myobject.to, myobject.eggs) 

Salida:

 dict_proxy({'to': 'and', '__module__': '__main__', 'args': 'spam', 'eggs': 'eggs', '__doc__': None}) spam and eggs 

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

Vale la pena decir que este estilo no es compatible con versiones anteriores de python 2. Si desea admitir ambos python 2 y 3, debe usar:

 from six import with_metaclass # or from future.utils import with_metaclass class Form(with_metaclass(MyMetaClass, object)): pass 

Esta es la forma más sencilla de pasar argumentos a una metaclase en Python 3:

Python 3.x

 class MyMetaclass(type): def __new__(mcs, name, bases, namespace, **kwargs): return super().__new__(mcs, name, bases, namespace) def __init__(cls, name, bases, namespace, custom_arg='default'): super().__init__(name, bases, namespace) print('Argument is:', custom_arg) class ExampleClass(metaclass=MyMetaclass, custom_arg='something'): pass 

También puede crear una clase base para las metaclases que solo usan __init__ con argumentos adicionales:

 class ArgMetaclass(type): def __new__(mcs, name, bases, namespace, **kwargs): return super().__new__(mcs, name, bases, namespace) class MyMetaclass(ArgMetaclass): def __init__(cls, name, bases, namespace, custom_arg='default'): super().__init__(name, bases, namespace) print('Argument:', custom_arg) class ExampleClass(metaclass=MyMetaclass, custom_arg='something'): pass