Metaclases en Python

Índice

Metaclases en Python

Metaclases de Python y metaprogramación










El término metaprogramación se refiere a la posibilidad de que un programa tenga conocimiento o se manipule a sí mismo. Python admite una forma de metaprogramación para clases llamadas metaclases .
Las metaclases son un concepto OOP esotérico , acechando detrás de prácticamente todos los códigos Python. Los estás usando ya sea que lo sepas o no. En su mayor parte, no necesita ser consciente de ello. La mayoría de los programadores de Python rara vez, o nunca, tienen que pensar en metaclases.
Sin embargo, cuando surge la necesidad, Python proporciona una capacidad que no todos los lenguajes orientados a objetos admiten: puede entenderse y definir metaclases personalizadas. El uso de metaclases personalizadas es un tanto controvertido, como lo sugiere la siguiente cita de Tim Peters, el gurú de Python autor del Zen de Python :
"Las metaclases son una magia más profunda de la que el 99% de los usuarios deberían 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

Hay Pythonistas (como se conoce a los aficionados a Python) que creen que nunca debes usar metaclases personalizadas. Eso podría ir un poco lejos, pero probablemente sea cierto que las metaclases personalizadas en su mayoría no son necesarias. Si no es bastante obvio que un problema lo requiera, entonces probablemente será más limpio y más legible si se resuelve de una manera más simple.
Aún así, la comprensión de las metaclases de Python vale la pena, ya que conduce a una mejor comprensión de las partes internas de las clases de Python en general. Nunca se sabe: un día puede encontrarse en una de esas situaciones en las que simplemente sabe que una metaclase personalizada es lo que quiere.

Clases de estilo antiguo vs. estilo nuevo

En el reino de Python, una clase puede ser una de dos variedades . No se ha decidido ninguna terminología oficial, por lo que se les llama informalmente clases de estilo antiguo y nuevo.

Clases de estilo antiguo

Con las clases de estilo antiguo, la clase y el tipo no son exactamente lo mismo. Una instancia de una clase de estilo antiguo siempre se implementa a partir de un único tipo incorporado llamado instanceSi objes una instancia de una clase antigua, obj.__class__designa la clase, pero type(obj)siempre lo es instance
>>> clase  Foo : 
...     pase 
... 
>>> x  =  Foo () 
>>> x . __class__ 
<clase __main__.Foo a las 0x000000000535CC48> 
>>> tipo ( x ) 
<tipo 'instancia'>

Clases de nuevo estilo

Las clases de estilo nuevo unifican los conceptos de clase y tipo. Si objes una instancia de una clase de estilo nuevo, type(obj)es lo mismo que obj.__class__:
>>> clase  Foo : 
...     pasar 
>>> obj  =  Foo () 
>>> obj . __class__ 
<clase '__main __. Foo'> 
>>> tipo ( obj ) 
<clase '__main __. Foo'> 
>>> obj . __class__  es  type ( obj ) 
True
>>> n  =  5 
>>> d  =  {  'x'  :  1 ,  'y'  :  2  }

>>> clase  Foo : 
...     pase 
... 
>>> x  =  Foo ()

>>> para  obj  en  ( n ,  d ,  x ): 
...     print ( tipo ( obj )  es  obj . __class__ ) 
... 
Verdadero 
Verdadero 
Verdadero

Tipo y clase

En Python 3, todas las clases son de estilo nuevo. Por lo tanto, en Python 3 es razonable referirse al tipo de un objeto y su clase indistintamente.
Recuerde que, en Python, todo es un objeto. Las clases también son objetos. Como resultado, una clase debe tener un tipo. ¿Cuál es el tipo de una clase?
Considera lo siguiente:
>>> clase  Foo : 
...     pase 
... 
>>> x  =  Foo ()

>>> escriba ( x ) 
<clase '__main __. Foo'>

>>> tipo ( Foo ) 
<clase 'tipo'>
El tipo de xes clase Foo, como era de esperar. Pero el tipo de Foo, la clase en sí, es typeEn general, el tipo de cualquier clase de estilo nuevo es type.
El tipo de clases incorporadas con las que está familiarizado también es type:
>>> para  t  en  int ,  float ,  dict ,  list ,  tuple : 
...     print ( type ( t )) 
... 
<class 'type'> 
<class 'type'> 
<class 'type'> 
<class ' tipo '> 
<clase' tipo '>
Para el caso, el tipo de typees typetambién (sí, realmente):
>>> tipo ( tipo ) 
<clase 'tipo'>
typees una metaclase, de la cual las clases son instancias. Así como un objeto ordinario es una instancia de una clase, cualquier clase de estilo nuevo en Python, y por lo tanto cualquier clase en Python 3, es una instancia de la typemetaclase.
En el caso anterior:
  • xes una instancia de clase Foo.
  • Fooes una instancia de la typemetaclase.
  • typetambién es una instancia de la typemetaclase, por lo que es una instancia de sí mismo.
cadena de clase

Definiendo una clase dinámicamente

La type()función incorporada, cuando se pasa un argumento, devuelve el tipo de un objeto. Para las clases de estilo nuevo, generalmente es lo mismo que el atributo del objeto__class__ :
>>> tipo ( 3 ) 
<clase 'int'>

>>> escriba ([ 'foo' ,  'bar' ,  'baz' ]) 
<class 'list'>

>>> t  =  ( 1 ,  2 ,  3 ,  4 ,  5 ) 
>>> tipo ( t ) 
<clase 'tuple'>

>>> clase  Foo : 
...     pasar 
... 
>>> tipo ( Foo ()) 
<clase '__main __. Foo'>
También puede llamar type()con tres argumentos type(<name>, <bases>, <dct>):
  • <name>especifica el nombre de la clase. Esto se convierte en el __name__atributo de la clase.
  • <bases>especifica una tupla de las clases base de las que hereda la clase. Esto se convierte en el __bases__atributo de la clase.
  • <dct>especifica un diccionario de espacio de nombres que contiene definiciones para el cuerpo de la clase. Esto se convierte en el __dict__atributo de la clase.
Llamar type()de esta manera crea una nueva instancia de la typemetaclase. En otras palabras, crea dinámicamente una nueva clase.
En cada uno de los ejemplos siguientes, el fragmento superior define dinámicamente una clase con type(), mientras que el fragmento debajo define la clase de la manera habitual, con la classinstrucción. En cada caso, los dos fragmentos son funcionalmente equivalentes.

Ejemplo 1

En este primer ejemplo, los argumentos <bases><dct>pasados ​​a type()son vacíos. No se especifica ninguna herencia de ninguna clase principal, y nada se coloca inicialmente en el diccionario de espacio de nombres. Esta es la definición de clase más simple posible:
>>> Foo  =  tipo ( 'Foo' ,  (),  {})

>>> x  =  Foo () 
>>> x 
<__ main__.Objeto de objeto en 0x04CFAD50>
>>> clase  Foo : 
...     pase 
... 
>>> x  =  Foo () 
>>> x 
<__ main__.Oculino objeto en 0x0370AD50>

Ejemplo 2

Aquí, <bases>hay una tupla con un solo elemento Foo, que especifica la clase padre de la que Barhereda. Un atributo attr,, se coloca inicialmente en el diccionario de espacio de nombres:
>>> Bar  =  tipo ( 'Bar' ,  ( Foo ,),  dict ( attr = 100 ))

>>> x  =  Bar () 
>>> x . attr 
100 
>>> x . __class__ 
<class '__main __. Bar'> 
>>> x . __class__ . __bases__ 
(<clase '__main __. Foo'>,)
>>> clase  Bar ( Foo ): 
...     attr  =  100 
...

>>> x  =  Bar () 
>>> x . attr 
100 
>>> x . __class__ 
<class '__main __. Bar'> 
>>> x . __class__ . __bases__ 
(<clase '__main __. Foo'>,)

Ejemplo 3

Esta vez, <bases>está nuevamente vacío. Dos objetos se colocan en el diccionario de espacio de nombres a través del <dct>argumento. El primero es un atributo nombrado attry el segundo una función llamada attr_val, que se convierte en un método de la clase definida:
>>> Foo  =  tipo ( 
...     'Foo' , 
...     (), 
...     { 
...         'attr' :  100 , 
...         'attr_val' :  lambda  x  :  x . Attr 
...     } 
... )

>>> x  =  Foo () 
>>> x . attr 
100 
>>> x . attr_val () 
100
>>> clase  Foo : 
...     attr  =  100 
...     def  attr_val ( self ): 
...         regresa a  ti mismo . attr 
...

>>> x  =  Foo () 
>>> x . attr 
100 
>>> x . attr_val () 
100

Ejemplo 4

Solo se pueden definir funciones muy simples lambdaen Python . En el siguiente ejemplo, una función un poco más compleja se define externamente y luego se asigna attr_valen el diccionario de espacio de nombres a través del nombre f:
>>> def  f ( obj ): 
...     print ( 'attr =' ,  obj . attr ) 
... 
>>> Foo  =  tipo ( 
...     'Foo' , 
...     (), 
...     { 
...         'attr' :  100 , 
...         'attr_val' :  f 
...     } 
... )

>>> x  =  Foo () 
>>> x . attr 
100 
>>> x . attr_val () 
attr = 100
>>> def  f ( obj ): 
...     print ( 'attr =' ,  obj . attr ) 
... 
>>> clase  Foo : 
...     attr  =  100 
...     attr_val  =  f 
...

>>> x  =  Foo () 
>>> x . attr 
100 
>>> x . attr_val () 
attr = 100

Metaclases personalizadas

Considera de nuevo este ejemplo gastado:
>>> clase  Foo : 
...     pase 
... 
>>> f  =  Foo ()
La expresión Foo()crea una nueva instancia de clase FooCuando el intérprete se encuentra Foo(), ocurre lo siguiente:
  • El __call__()método de Foola clase padre se llama. Dado que Fooes una clase de estilo nuevo estándar, la clase padre es la typemetaclase, por lo que type's __call__()se invoca el método.
  • Ese __call__()método a su vez invoca lo siguiente:
    • __new__()
    • __init__()
Si Foono define __new__()__init__(), los métodos predeterminados se heredan de Foosu ascendencia. Pero si Foodefine estos métodos, anulan los de la ascendencia, lo que permite un comportamiento personalizado al crear instancias Foo.
A continuación, new()se define y asigna un método personalizado llamado como el __new__()método para Foo:
>>> def  new ( cls ): 
...     x  =  objeto . __new__ ( cls ) 
...     x . attr  =  100 
...     devuelve  x 
... 
>>> Foo . __new__  =  new

>>> f  =  Foo () 
>>> f . attr 
100

>>> g  =  Foo () 
>>> g . attr 
100
Esto modifica el comportamiento de instanciación de la clase Foo: cada vez que Foose crea una instancia de , de forma predeterminada se inicializa con un atributo llamado attr, que tiene un valor de 100(Este tipo de código aparecería más habitualmente en el __init__()método y no en el típico __new__(). Este ejemplo está ideado para fines de demostración).
Ahora, como ya se ha reiterado, las clases también son objetos. Supongamos que desea personalizar de forma similar el comportamiento de creación de instancias al crear una clase como FooSi fuera a seguir el patrón anterior, definiría nuevamente un método personalizado y lo asignaría como el __new__()método para la clase de la cual Fooes una instancia. Fooes una instancia de la typemetaclase, por lo que el código se ve más o menos así:
# Alerta de spoiler: ¡Esto no funciona! 
>>> def  new ( cls ): 
...     x  =  tipo . __new__ ( cls ) 
...     x . attr  =  100 
...     devuelve  x 
... 
>>> tipo . __new__  =  nueva 
Rastreo (llamada más reciente pasado): 
  File "<pyshell # 77>" , línea 1 , en <módulo> 
    Tipo . __new__  =  new 
TypeError :no puede establecer atributos de tipo de extensión / incorporado 'tipo'
Excepto que, como puede ver, no puede reasignar el __new__()método de la typemetaclase. Python no lo permite.
Esto probablemente sea igual de bueno. typees la metaclase de la que se derivan todas las clases de estilo nuevo. Realmente no deberías estar jugando con eso de todos modos. Pero, ¿qué recurso hay allí, si desea personalizar la creación de instancias de una clase?
Una posible solución es una metaclase personalizada. Esencialmente, en lugar de perder el tiempo con la typemetaclase, puedes definir tu propia metaclase, de la cual deriva type, y luego puedes jugar con ella en su lugar.
El primer paso es definir una metaclase que se deriva de type, de la siguiente manera:
>>> clase  Meta ( tipo ): 
...     def  __new__ ( cls ,  nombre ,  bases ,  dct ): 
...         x  =  super () . __new__ ( cls ,  nombre ,  bases ,  dct ) 
...         x . attr  =  100 
...         devolver  x 
...
El encabezado de definición class Meta(type):especifica de dónde se Metaderiva typeDado que typees una metaclase, también Metacrea una metaclase.
Tenga en cuenta que __new__()se ha definido un método personalizado para MetaNo fue posible hacer eso en la typemetaclase directamente. El __new__()método hace lo siguiente:
  • Los delegados viajan super()al __new__()método de la metaclase padre ( type) para crear una nueva clase
  • Asigna el atributo personalizado attra la clase, con un valor de100
  • Devuelve la clase recién creada
Ahora la otra mitad del vudú: defina una nueva clase Fooy especifique que su metaclase es la metaclase personalizada Meta, en lugar de la metaclase estándar typeEsto se hace usando la metaclasspalabra clave en la definición de clase de la siguiente manera:
>>> clase  Foo ( metaclass = Meta ): 
...     pasar 
... 
>>> Foo . attr 
100
Voila! Fooha recogido el attratributo automáticamente de la Metametaclase. Por supuesto, cualquier otra clase que defina de manera similar hará lo mismo:
>>> clase  Bar ( metaclass = Meta ): 
...     pasar 
... 
>>> clase  Qux ( metaclass = Meta ): 
...     pasar 
... 
>>> Bar . attr ,  Qux . attr 
(100, 100)
De la misma manera que una clase funciona como una plantilla para la creación de objetos, una metaclase funciona como una plantilla para la creación de clases. Las metaclases a veces se denominan fábricas de clase .
Compare los siguientes dos ejemplos:
Fábrica de Objetos:
>>> clase  Foo : 
...     def  __init__ ( self ): 
...         self . attr  =  100 
...

>>> x  =  Foo () 
>>> x . attr 
100

>>> y  =  Foo () 
>>> y . attr 
100

>>> z  =  Foo () 
>>> z . attr 
100
Fábrica de clase:
>>> clase  Meta ( tipo ): 
...     def  __init__ ( 
...         cls ,  nombre ,  bases ,  dct 
...     ): 
...         cls . attr  =  100 
... 
>>> clase  X ( metaclase = Meta ): 
...     pasar 
... 
>>> X . attr 
100

>>> clase  Y ( metaclase = Meta ): 
...     pasar 
... 
>>> Y . attr 
100

>>> clase  Z ( metaclase = Meta ): 
...     pasar 
... 
>>> Z . attr 
100

¿Es esto realmente necesario?

Tan simple como el ejemplo de fábrica de clase anterior es, es la esencia de cómo funcionan las metaclases. Permiten la personalización de la creación de instancias de clase.
Aún así, esto es mucho alboroto solo para otorgar el atributo personalizado attren cada clase recién creada. ¿Realmente necesitas una metaclase solo para eso?
En Python, hay al menos otras dos maneras en las que efectivamente se puede lograr lo mismo:
Herencia simple:
>>> clase  Base : 
...     attr  =  100 
...

>>> clase  X ( Base ): 
...     pase 
...

>>> clase  Y ( Base ): 
...     pase 
...

>>> clase  Z ( Base ): 
...     pase 
...

>>> X . attr 
100 
>>> Y . attr 
100 
>>> Z . attr 
100
Decorador de clase:
>>> def  decorator ( cls ): 
...     clase  NewClass ( cls ): 
...         attr  =  100 
...     regresa  NewClass 
... 
>>> @decorator 
... clase  X : 
...     pasa 
... 
>>> @decorator 
... clase  Y : 
...     pasar 
... 
>>> @decorator 
... clase  Z : 
...     pasar 
...

>>> X . attr 
100 
>>> Y . attr 
100 
>>> Z . attr 
100
Recuerda que puedes Aprender Python de manera fácil aquí en Mi Diario Python.
  1. Anónimo dice:

    Conozco esta página desde hace relativamente poco tiempo, pero su contenido me ha parecido bastante interesante y practico, hasta que he visto este articulo, simple `copy and paste` pasando por google translate chapucero del blog realpython, este en concreto, https://realpython.com/python-metaclasses/, que me ha decepcionado bastante.
    No es que me parezca mal que el contenido no sea original, es de agradecer su labor compartiendo sus concimientos. Lo feo del asunto es la forma de hacerlo, ya que, como mínimo, debería de haberse citado la fuente original, y ya que nos ponemos a traducir (siempre con el consentimiento del autor) hacerlo bien, vamos, que hasta las sentencias de los ejemplos están traducidas, hotlink de las imágenes incluido.
    Espero que rectifiquen y al menos citen la fuente original, y tengan en cuenta esto en futuros artículos, ya que estas cosas restan calidad al blog, lo que es una pena.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir
White Monkey