Ahí va la virgen! Metaclases! (con Python)
Esta receta es una pequeña introducción a la meta-programación, concretamente voy a contar algunas cosillas sobre uno de los mecanismos más populares y potentes: las metaclases.
Introducción
¿Qué es una metaclase? Pues es una clase cuyas instancias son clases en lugar de objetos. Es decir, si para construir un objeto usas una clase, para construir una clase usas una metaclase.
¿Y eso para qué sirve? Pues resulta muy útil principalmente para dos cosas:
- Cuando no sea posible determinar el tipo de un objeto hasta el momento de la ejecución del programa, o cuando sea necesario crear una clase a la medida de las circunstancias. Se podría decir que en este caso la metaclase funciona como una “fábrica de clases” especializada.
- Cuando se desea componer o modificar el comportamiento o características de una clase en el momento de su creación por medio de herencia o por mecanismos de construcción dinámicos. Es algo parecido a la Programación Orientada a Aspectos o como una generalización del patrón decorator.
Nota: Aunque es perfectamente posible (aunque doloroso) crear metaclases con “clases clásicas”, en esta receta me voy a centrar únicamente en el uso de clases del “nuevo-estilo”, aunque ya no son muy nuevas que digamos; aparecieron en Python 2.1. Si no sabes de qué estoy hablando lee New-style and classic classes.
Requisitos
Para que puedas sacarle todo el juego a esta receta necesitas:
- Tener claros conceptos típicos de orientación a objetos: herencia, encapsulación, polimorfismo, etc
- Tener unas nociones sobre patrones de diseño
- Tener una buena base de Python y sus mecanismos de introspección
- Probar tú mismo los ejemplos que aparecen aquí
No voy a tratar aquí todo lo que se puede hacer con metaclases (sería una tarea titánica). Mi objetivo es que esta receta sirva para entender las bases del invento y poder enfrentarse con una base sólida a la literatura sobre metaclases que se puede encontrar por ahí, por ejemplo en la sección Referencias de esta misma receta.
Hay un cierto elitismo que rodea a todo esto de la metaprogramación y que me fastidia bastante. La mayoría de lo que puedes encontrar por Internet relacionado con este tema da por hecho que no te vas a enterar absolutamente de nada. Mi intención con esta receta es demostrar que no es algo tan difícil (nótese la ironía del título de la receta) y que está al alcance de cualquier programador inquieto. Juzga tú si he conseguido mi propósito.
Un problema
Imagina que tienes un conjunto de clases:
Y tu programa debe crear un objeto de una de esas clases, pero lo que determina qué clase instanciar es un dato introducido por el usuario. En principio, las dos alternativas para implementar eso que se le pueden ocurrir a uno a bote pronto no pintan demasiado bien:
- Una estructura de selección múltiple “if elif” monstruosa.
- Un diccionario en el que las claves son nombres de clase y los valores son las propias clases. Es decir, algo como:
Esto funciona bien cuando no necesitas manejar herencia, simplemente quieres elegir qué clase instanciar. Sin embargo, Python dispone de una forma de construir y ejecutar código dinámicamente dentro de un programa, usando la función exec()
. Así que podrías hacer:
Y esto realmente ya es metaprogramación, pero sigue sin ser suficiente si lo que quieres es crear realmente una clase nueva y no sólo elegir entre ciertas clases ya existentes. Usar exec()
para cosas más sofisticadas que ese ejemplo resulta confuso y potencialmente problemático, aunque también es uno de los mecanismos de metaprogramación más potentes en Python, por razones obvias.
type, la madre de todas las metaclases
A partir de Python 2.2 type()
tiene un significado nuevo muy especial: permite crear nuevos tipos, es decir, es una metaclase, de hecho es la metaclase con la que están creados todos los tipos “built-in” de Python y todas las clases de “nuevo-estilo” (las que heredan de object
); como ya he dicho, obviaré las “clases clásicas”.
Para crear una clase con type()
se usa la siguiente sintaxis:
- name: es el nombre de la nueva clase
- bases: son las clases de las que hereda
- dct: es un diccionario con los métodos que implementa
Por ejemplo, puedes crear una nueva clase llamada Saludo
que implementa un método hola()
simplemente con:
Y de hecho, cualquier sentencia class
de las que has usado hasta ahora implica realmente una invocación similar, puesto que type
es la metaclase implícita cuando defines clases “normales”. Sin embargo, en el ejemplo anterior, type
no se usa como metaclase; se emplea simplemente como “factoría de clases” puesto que se invoca como una función (o al menos, es lo que parece). Es decir, salvando las distancias, es algo equivalente a:
Pero type
no es una función, es una metaclase (una clase) y por tanto se puede heredar de ella. Y como type
es una metaclase: cualquier subclase de type
ES una metaclase; y aquí es dónde la cosa se pone interesante :-)
Antes de seguir: __new__ e __init__
__new__
e __init__
son dos de los métodos especiales que tienen todas las clases. Es muy habitual que el programador escriba un método __init__
para sus clases (tanto que ni siquiera voy a poner un ejemplo). Este método se ejecuta después de construirse la instancia y se utiliza para inicializar los atributos que lo requieran y, en general, hacer trabajo de “setup” de la instancia.
El uso de __new__
es mucho menos frecuente, hasta el punto de que muchos programadores con muchas horas de vuelo en Python nunca lo han usado. __new__
es el verdadero “constructor”, es el encargado de crear la instancia y proporcionarle una ubicación en memoria.
__new__
es un método peculiar. Es estático (staticmethod
), es decir, existe con independencia de las instancias de la clase y por tanto no tiene un argumento self
. En su lugar, lo que se le pasa como argumento es la propia clase, normalmente nombrado como cls
.
El proceso de creación de un objeto (instancia) de una clase es más o menos así (después lo refinamos):
- Se invoca la clase con los argumentos requeridos
- Se ejecuta el método
__new__
pasándose la clase a sí misma como primer argumento, y a continuación los argumentos que indicó el usuario en la “invocación” original. __new__
retorna una nueva instancia (esto es obligatorio).- Se ejecuta el método
__init__
pasando como primer argumento la instancia creada por__new__
y también todos los argumentos de la invocación original.
Después veremos que en realidad todo esto está orquestado por la metaclase.
Escribiendo un método __new__
Como ejemplo, esta es una clase que formatea una cadena como título (pone en mayúscula la primera letra de cada palabra). Lo interesante es que esta clase ES UNA cadena, es decir, hereda del tipo predefinido str
:
Aunque sea un ejemplo muy simple y no tenga mucha utilidad, demuestra la forma en que se construye el objeto y permite algo que no puede hacerse de otro modo; puede decidir el valor de un objeto de tipo inmutable (como es str
) en el único momento en que es posible: su creación. Si quieres, puedes comprobar que esto no se puede hacer utilizando __init__
.
La sentencia return
invoca directamente el método __new__
de la superclase porque ése es el único modo de conseguir una creación limpia, es decir, evitando que se ejecute el método __init__
de la superclase. Para este ejemplo, lo podrías cambiar por:
y funcionará igual, porque el método __init__
de str
no hace ni puede hacer nada relevante sobre la instancia. Pero para clases más complejas, construir la instancia de ese modo en lugar de usar __new__
puede tener efectos no deseables.
Tu primera metaclase
La metaclase más simple (y más inútil) que se puede escribir es algo como:
Es decir, no es más que un “alias” de type
y por tanto hereda todos sus métodos. Ahora añade un método a la metaclase, llamado por algunos ‘metamétodo’. Yo no estoy de acuerdo con esa denominación, porque siendo puristas un metamétodo sería un método de un método (que los hay: str.lower.__call__
) pero esto es solo un método de una metaclase, que poco tiene que ver.
De los dos primeros intentos de ejecutar el método habla()
se puede concluir que para que un método definido en la metaclase pueda ser invocado se requiere de una instancia (una clase); eso no resulta sorprendente, es lo mismo que ocurre con los métodos normales en las clases normales.
Y del tercer intento se deduce que las instancias de las clases creadas con la metaclase no tienen acceso a los métodos definidos en la metaclase. O sea, los métodos definidos en la metaclase son para las clases creadas con la metaclase. Es algo parecido a definir ese método en la clase A usando el decorador @classmethod
; la diferencia más importante es que con el método de clase (@classmethod
), las subclases de A heredan el método, sin embargo, con el código del ejemplo no ocurre.
La vuelta de tuerca
Después de estos preliminares, puedes empezar a atar cabos. Si cuando hablamos de clases, __new__
sirve para crear instancias e __init__
sirve para inicializar/modificar instancias… cuando hablemos de metaclases:
__new__
sirve para crear clases__init__
sirve para inicializar/modificar clases
Es decir, los dos usos principales de las metaclases que vimos en la introducción. Eso no significa que estos sean los dos únicos métodos interesantes de las metaclases.
Lo interesante aquí es que, “modificar una clase” incluye cosas como añadir o modificar métodos, sus implementaciones, sus nombres, argumentos y número, atributos, etc, etc, las posibilidades son infinitas.
Empieza por redefinir __init__
y __new__
invocando a los métodos de la superclase. Esto tampoco es que sea una maravilla, pero sirve para que veas cuando se ejecuta cada cosa y puedas comprobar que lo que dije antes sobre __new__
e __init__
para clases también funciona con metaclases (será porque las metaclases son clases ;-))
Ya toca escribir una metaclase que haga algo útil por poco que sea. Esta metaclase crea automáticamente métodos que se llaman "not_[método]’ para cada método de la clase. Estos métodos devuelven el valor lógico negado de la función original.
Un poco de azúcar sintáctico
Supongo que eso de crear clases pasando el nombre, la lista de clases base y el diccionario de métodos te parecerá tan ortopédico como a mí. No hay problema, afortunadamente hay una forma mucho más estilosa de usar una metaclase. Consiste en usar un atributo de clase especial llamado __metaclass__
y asignarle la metaclase que quieras usar para crear esa clase. Lo siguiente es equivalente al ejemplo anterior pero modificado para usar __metaclass__
:
Ejemplos
- Patrón Singleton como metaclase
- Patrón Flyweight en Python como metaclase
- Invocación automática del ‘constructor’ de la superclase
Referencias
Los ejemplos de esta receta están fuertemente inspirados en las referencias que cito a continuación. He tratado de simplificarlos para hacerlos más accesibles al programador Python novato. Es muy probable que en el proceso de “simplificación” haya metido la pata y tengan algún que otro error gordo, por favor, házmelo saber con un comentario.