martes, 18 de mayo de 2010

Hablando de metaclases en python

De manera super resumida y algo recursiva, una metaclase es para una clase, lo que una clase es para un objeto.

pero la metaclase la podemos tratar como si fuese una clase

veamos el siguiente ejemplo


class MetaPersona(type):
pass


class Persona(object):
__metaclass__ = MetaPersona

p = Persona()


Decifrando el trabalengua anterior, si para construir objeto(p) se necesita una clase(Persona), para construir la clase "Persona", python usa la metaclase "MetaPersona", que se ha especificado en la linea 6. Si obviamos esta linea, como es lo habitual, se usaria "type", que es la metaclase por defecto.

El ejemplo anterior tambien podria haberse especificado la metaclase de la siguiente manera

Persona = MetaPersona('Persona', (), {})

puesto que una metaclase es una clase, lo que hemos hecho es constriur una instancia de la metaclase, el contructor de la metaclase tiene la forma:

type(name, bases, dct)

name: nombre de la nueva clase
bases: clases bases de la nueva clase
dct: diccionario con las definiciones para la nueva claseclass.

Ahora veamos "in a nutshell" como ocurre el proceso de creacion de una instancia, para luego entender mejor el proceso de creacion de una clase.

En que orden ocurre el proceso de creacion de la instancia?

1* Se invoca la clase con los argumentos requeridos
2* 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. Este metodo debe retornar una nueva instancia.
3* 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.

El siguiente codigo sirve de practica para comprobar lo anterior:

class Persona(object):
def __new__(cls, *args, **kwargs):
print "creacion de la instancia"
return super(Persona, cls).__new__(cls, args, kwargs)
def __init__(self, *args, **kwargs):
print "inicializando la instancia"
super(Persona, self).__init__(self, args, kwargs)

compruebelo usted mismo usando un shell y obtendra lo siguiente


>>>p = Persona()
creacion de la instancia
inicializando la instancia

TRASPOLANDO EL PROCESO DE CREACION DE UNA INSTANCIA AL PROCESO DE CREACION DE UNA CLASE

Por suerte para todos, el mismo proceso de creacion de instancias, tiene lugar cuando se crea una clase, de ahi que la metaclase tambien tiene un metodo __new__ y un metodo __init__, con la diferencia, de que, el metodo __new__ y el metodo __init__ reciben como primer parametro, la metaclase y la clase creada por el metodo __new__ respectivamente.


class MetaPersona(type):
def __new__(meta, name, base, dct):
return super(MetaPersona, meta).__new__(meta, name, base, dct)

def __init__(cls, name, bases, dct):
super(MetaPersona, cls).__init__(name, bases, dct)


class Persona(object):
__metaclass__ = MetaPersona

>>creando la clase
>>inicializacion de la clase


DEFINIENDO METODOS EN LA METACLASE

Normalmente para definir metodos de clase, lo hacemos dentro de la clase como se refleja a continuacion


class Persona(object):

@classmethod
def prefix_nombre_clase(cls, prefix):
return prefix + cls.__name__

Persona.prefix_nombre_clase("esta es la clase ")


vale aclarar que @classmethod es un decorador en python y es equivalente a decir


class Persona(object):

def prefix_nombre_clase(cls, prefix):
return prefix + cls.__name__

prefix_nombre_clase = classmethod(prefix_nombre_clase)


pero tambien pudiesemos hacerlo como metodo de la metaclase


class MetaPersona(type):

def prefix_nombre_clase(cls, prefix):
return prefix + cls.__name__

class Persona(objet):
__metaclass__ = MetaPersona

Persona.prefix_nombre_clase("esta es la clase ")


Hay realmente alguna diferencia??

Pues sip, una de ellas es que usando @classmethod, el metodo aun pudiese llamarse a partir de una instancia de la clase. O sea:


p = Persona()
p.prefix_nombre_clase("prefijo")


equivaldria a llamar al metodo desde la clase a la que pertenece el objeto. Sin embargo si definimos el metodo dentro de la metaclase, solo pudiesemos llamarlo desde la clase, de lo contrario obtendriamos el siguiente mensaje de error:

AttributeError: 'Persona' object has no attribute 'prefix_nombre_clase'

DONDE NOS PODRIAN AYUDAR EL USO DE LAS METACLASES

Supongamos que queremos abtraernos en la clase BasePersona, las caracteristicas comunes a todas las personas, para que luego otros programadores, hereden de esta y definar especializaciones como Estudiante, Trabajador, etc. Como mecanismo de validacion, se quiere que dado una lista fija de keywords, se valide, que ninguna clase que herede de BasePersona, pueda definir un atributo, que coincida con alguna de los keywords.



RESERVED_WORDS = ['departamento', 'sancionado']

class Meta(type):
def __init__(cls, name, bases, dct):
for attr_name in dct.keys():
if attr_name in RESERVED_WORDS:
raise Exception

class BasePersona(object):
__metaclass__ = Meta
nombre = ''
apellidos = ''

class Estudiante(BasePersona):
curso = ''
aula = ''

class Trabajador(BasePersona):
especialidad = ''
departamento = ''


LAS METACLASES Y LOS MODELOS EN DJANGO

Justamente antes de decidir escribir estas lineas, me invadia constantemente una pregunta, cuando definimos un modelo en


from django.db import models

class Persona(models.Model)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)

class Meta:
db_table = 'd_personas'
verbose_name_plural = "personas"


Es la clase "Meta" una metaclase?, si no, como se entiende, que permita definir meta opciones para un modelo especifico?

Realmente la clase Meta, no es mas que una clase interna que se usa para contener descriptores que se usaran en el mecanismo de creacion de la clase en si. Como logran hacer esto? pues usando metaclases. A modo de informacion, la clase Model, de django, usa como metaclase ModelBase, a continuacion un fragmento del codigo:


class ModelBase(type):
"""
Metaclass for all models.
"""
def __new__(cls, name, bases, attrs):
"""
MIS COMENTARIOS
intercepta el mecanismo de creacion de la clase
leer la clase interna Meta, y basado en los atributos que
contenga modifica la creacion de la clase del que hereda de Model

agrega el atributo "_meta", a la clase del modelo, para acceder
a todos los atributos de la clase interna Meta
si lo desea puede remitirse a django.db.models.base.py line 25

...
...
...
"""

class Model(object):
__metaclass__ = ModelBase

...
...

Vemos como en esta situacion, se aprovecha el proceso de creacion de la clase para interceptar la forma en que esta se crea

LAS METACLASES Y LOS MODELOS EN APPENGINE


from google.appengine.ext import db

class Persona(db.Model):
first_name = db.StringProperty()
last_name = db.StringProperty()

esta clase Model, hace uso de las metaclases para intervenir el proceso de inicializacion de las propiedades del modelo y hacer algunas validaciones como no permitir usar determinadas keywords como atributos, y evitar duplicados en los atributos de los modelos.


class PropertiedClass(type):

def __init__(cls, name, bases, dct, map_kind=True):
super(PropertiedClass, cls).__init__(name, bases, dct)

_initialize_properties(cls, name, bases, dct)

if map_kind:
_kind_map[cls.kind()] = cls

class Model(object):
__metaclass__ = PropertiedClass

Vemos como en este ejemplo, se aprovecha el metodo __init__ de la metaclase, para intervenir en la inicializacion