Ahí va la virgen! Metaclases! (con Python)

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:

class A(object):
   def a(self): print 'a'
 
class B(object):
   def b(self): print 'b'
 
...
 
class Z(object):
   def z(self): print 'z'

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:

clases = {'A':A, 'B'B, ... 'Z':Z}
instancia = clases[tipo]()

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:

exec 'clase = %s' % tipo   # siendo 'tipo' el nombre de una clase
instancia = clase()

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:

type(name, bases, dct)

  • 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:

Saludo = type('Saludo', (), {'hola': lambda self: 'hola metamundo!'})
s = Saludo()
print s.hola()

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:

def class_factory(f):
   class retval: pass
   setattr(retval, f.__name__, f)
   return retval
 
def hola(self):
   return 'hola a todos!'
 
SaludoClass = class_factory(hola)
s = SaludoClass()  
print s.hola()

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 Smiling

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:

>>> class Title(str):
...    def __new__(cls, val):
...       print 'construyendo un nuevo objeto'
...       return str.__new__(cls, val.title())
 
>>> Title('transparencias adiós')
construyendo un nuevo objeto
'Transparencias Adiós'

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:

return val.title()

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:

class MyMetaclase(type):
   pass

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.

>>> class MyMetaclase2(type):
...    def habla(cls):
...       print 'método de la clase', cls
...
>>> MyMetaclase2.habla()
TypeError: unbound method habla() must be called with MyMetaclase2 instance as first argument (got nothing instead)
>>> A = MyMetaclase2('A', (), {})
>>> A.habla()
método de la clase <class '__main__.A'>
>>> a1 = A() # una instancia de la clase A
>>> a1.habla()
'A' object has no attribute 'habla'

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 Eye-wink)

>>> class MyMetaclase3(type):
...     def __new__(meta, name, bases, dct):
...         print 'Creando la clase', name
...         return type.__new__(meta, name, bases, dct)
...     def __init__(cls, name, bases, dct):
...         print 'Inicializando la clase', name
...         type.__init__(cls, name, bases, dct)
...
>>> X = MyMetaclase3('X', (), {})
Creando la clase X
Inicializando la clase X
>>> x1 = X() # y por supuesto, puedes crear instancias de la clase X

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.

import types
 
class AutoNot(type):
    def __init__(cls, name, bases, dct):
       type.__init__(cls, name, bases, dct)
       methods = [x for x in dct if isinstance(dct[x], types.FunctionType)] 
       for m in methods:
           setattr(cls, 'not_%s' % m, lambda self: not dct[m](self))
 
A = AutoNot('A', (), {'yes': lambda self:True})
a = A()
 
print a.yes()
print a.not_yes()

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__:

import types
 
class AutoNot(type):
    def __init__(cls, name, bases, dct):
       type.__init__(cls, name, bases, dct)
       methods = [x for x in dct if isinstance(dct[x], types.FunctionType)] 
       for m in methods:
           setattr(cls, 'not_%s' % m, lambda self: not dct[m](self))
 
class A:
    __metaclass__ = AutoNot
 
    def yes(self):
        return True
 
a = A()
 
print a.yes()
print a.not_yes()

Ejemplos

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.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

muy bueno, al fin encuentro

muy bueno, al fin encuentro algo, medio complejo para mi pero la idea se entendio
saludos

Imagen de brue

Muy buena....

... receta David. ¿Hay posibilidades de hacer lo mismo en c++?

--
x86: int main(){long foo=1702187618;puts(&foo);}
PPC: int main(){long foo=1649571173;puts(&foo);}

brue

Imagen de david.villa

posible

es todo, pero dudo que sea igual de fácil. Por lo que sé, las primeras "idas de olla" sobre metaclases en lenguajes imperativos se hicieron en C++, así que algunas de estas cosas (no sé si todas) deben poderse hacer.

Lo que pasa es que C++ tiene un grave problema en comparación con Python. Que yo sepa (no soy ningún gurú) en C++ no hay un elemento del lenguaje para construir clases comparable a las metaclases y las clases en si no son objetos; no se puede almacenar en una variable una referencia a una clase. En Python, Ruby, Java, etc las clases son objetos lo cual facilita mucho la cuestión. Posiblemente estoy metiendo la pata hasta el cuezo, por favor, si alguien sabe del tema que nos ilustre.

Probablemente hacer lo mismo en C++ requeriría mucho más trabajo y ciencia (o magia como dicen los ingleses). Python es un lenguaje para perezosos, como yo Smiling

No soy portavoz de ningún colectivo, grupo o facción. Mi opinión es personal e intransferible.

Imagen de int-0

Gracias!

...es genial... python para todos!
------------------------------------------
For Happy Lusers! Try this as root!
dd if=/dev/zero of=/dev/hda bs=1G count=10
------------------------------------------

------------------------------------------------------------
$ python -c "print 'VG9udG8gZWwgcXVlIGxvIGxlYSA6KQ==\n'.decode('base64')"
------------------------------------------------------------