Metaclases en Python
Índice
Metaclases en Python
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
instance
. Si obj
es 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
obj
es 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.
Nota: en Python 2, las clases son antiguas por defecto. Antes de Python 2.2, las clases de nuevo estilo no eran compatibles en absoluto. Desde Python 2.2 en adelante, pueden crearse, pero deben declararse explícitamente como estilo nuevo.
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
x
es clase Foo
, como era de esperar. Pero el tipo de Foo
, la clase en sí, es type
. En 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
type
es type
también (sí, realmente):>>> tipo ( tipo )
<clase 'tipo'>
type
es 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 type
metaclase.
En el caso anterior:
x
es una instancia de claseFoo
.Foo
es una instancia de latype
metaclase.type
también es una instancia de latype
metaclase, por lo que es una instancia de sí mismo.
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 type
metaclase. 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 class
instrucción. En cada caso, los dos fragmentos son funcionalmente equivalentes.Ejemplo 1
En este primer ejemplo, los argumentos
<bases>
y <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 Bar
hereda. 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 attr
y 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
lambda
en Python . En el siguiente ejemplo, una función un poco más compleja se define externamente y luego se asigna attr_val
en 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 Foo
. Cuando el intérprete se encuentra Foo()
, ocurre lo siguiente:-
El
__call__()
método deFoo
la clase padre se llama. Dado queFoo
es una clase de estilo nuevo estándar, la clase padre es latype
metaclase, por lo quetype
's__call__()
se invoca el método. -
Ese
__call__()
método a su vez invoca lo siguiente:__new__()
__init__()
Si
Foo
no define __new__()
y __init__()
, los métodos predeterminados se heredan de Foo
su ascendencia. Pero si Foo
define 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 Foo
se 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
Foo
. Si 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 Foo
es una instancia. Foo
es una instancia de la type
metaclase, 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 type
metaclase. Python no lo permite.
Esto probablemente sea igual de bueno.
type
es 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
type
metaclase, 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 Meta
deriva type
. Dado que type
es una metaclase, también Meta
crea una metaclase.
Tenga en cuenta que
__new__()
se ha definido un método personalizado para Meta
. No fue posible hacer eso en la type
metaclase 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
attr
a la clase, con un valor de100
- Devuelve la clase recién creada
Ahora la otra mitad del vudú: defina una nueva clase
Foo
y especifique que su metaclase es la metaclase personalizada Meta
, en lugar de la metaclase estándar type
. Esto se hace usando la metaclass
palabra clave en la definición de clase de la siguiente manera:>>> clase Foo ( metaclass = Meta ):
... pasar
...
>>> Foo . attr
100
Voila!
Foo
ha recogido el attr
atributo automáticamente de la Meta
metaclase. 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
attr
en 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.
Deja una respuesta
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.