Metaprogramación con Python 3
Andrey Antukh | www.niwi.be | @niwibe | github.com/niwibe
Las clases
Consideramos esta clase como ejemplo:
class Spam(object):
def __init__(self, name):
self.name = name
def say_hello(self):
print("Hello {0}".format(self.name))
Podemos deducir estos datos:
Nombre: “Spam”
Bases: “object”
Metodos: “__init__” y “say_hello”
¿Como se construye una clase?
Como primer paso, se crea un dict donde se van a almacenar los
atributos de clase:
>>> cls_attrs = type.__prepare__()
>>> type(cls_attrs)
<class 'dict'>
¿Como se construye una clase?
Como segundo paso, se extrae el “body” de la clase, o es decir lo
que representa la definicion de los metoros y atributos:
>>> body = """
def __init__(self, name):
self.name = name
def say_hello(self):
print("Hello {0}".format(self.name))
"""
¿Como se construye una clase?
Como tercer paso, se compila el “body” extraido y se rellena el
contexto de la clase:
>>> exec(body, globals(), clsattrs)
>>> clsattrs
{'say_hello': <function say_hello at 0x7f0b840e5e60>,
'__init__': <function __init__ at 0x7f0b840e5dd0>}
¿Como se construye una clase?
Como cuarto paso, se crea un nuevo tipo:
>>> Spam = type("Spam", (object,), clsattrs)
>>> Spam
<class '__main__.Spam'>
>>> instance = Spam("Andrey")
>>> instance.say_hello()
Hello Andrey
¿Que es metaclase?
Metaclase, es la clase responsible de crear clases:
class SomeMeta(type):
def __new__(clst, name, bases, attrs):
print("SomeMeta.__new__", clst, name, bases, {})
return super().__new__(clst, name, bases, attrs)
def __init__(cls, name, bases, attrs):
print("SomeMeta.__init__", cls, name, bases, {})
super().__init__(name, bases, attrs)
def __call__(cls, *args, **kwargs):
print("SomeMeta.__call__", cls, args, kwargs)
return super().__call__(*args, **kwargs)
class A(metaclass=SomeMeta):
def __new__(cls, *args, **kwargs):
print("A.__new__", cls, args, kwargs)
return super().__new__(cls)
def __init__(self, *args, **kwargs):
print("A.__init__", self, args, kwargs)
a = A(2)
¿Que es metaclase?
Ejecución en una shell interactiva:
~ python -i p1_meta.py
SomeMeta.__new__ <class '__main__.SomeMeta'> A () {}
SomeMeta.__init__ <class '__main__.A'> A () {}
SomeMeta.__call__ <class '__main__.A'> (2,) {}
A.__new__ <class '__main__.A'> (2,) {}
A.__init__ <__main__.A object at 0x7fc5f4b01b10> (2,) {}
¿Que es cada cosa?
● meta.__new__: se encarga de crear la metaclase
● meta.__init__: se encarga de inicializar la metaclase
● meta.__call__: hace que la clase que se crea a partir de esta meta clase sea callable
● A.__new__: se encargade crear la instancia
● A.__init__: se encarga de inicializar la instancia
¿Que es metaclase?
Metaclase
Clase
Instancia de?
Instancia de?
Objecto (instancia)
Constructor generico
class Person(object):
def __init__(self, first_name, last_name, birthday,
location, zipcode, country, sex):
self.first_name = first_name
self.last_name = last_name
self.birthday = birthday
self.location = location
self.zipcode = zipcode
self.country = country
self.sex = sex
Constructor generico
Vamos a definir estructuras de datos y
queremos eliminar la repeticion de definicion
del metodo __init__.
Constructor generico
Primera aproximación:
from inspect import Signature, Parameter
class Struct(object):
_fields = []
def __init__(self, *args, **kwargs):
params = [Parameter(field, Parameter.POSITIONAL_OR_KEYWORD)
for field in self._fields]
sig = Signature(params)
bound_values = sig.bind(*args, **kwargs)
for name, value in bound_values.arguments.items():
setattr(self, name, value)
class Point(Struct):
_fields = ["x", "y"]
Constructor generico
Ejemplo de uso en una shell:
~ python -i example3-signatures.py
>>> p = Point(2, 5)
>>> p.x, p.y
(2, 5)
>>> p = Point(2, 7, z=2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example3-signatures.py", line 12, in __init__
bound_values = sig.bind(*args, **kwargs)
File "/usr/lib64/python3.3/inspect.py", line 2036, in bind
return __bind_self._bind(args, kwargs)
File "/usr/lib64/python3.3/inspect.py", line 2027, in _bind
raise TypeError('too many keyword arguments')
TypeError: too many keyword arguments
Comprobación de tipo de atributos
Primera aproximación, usando properties:
class Point(Struct):
_fields = ["x", "y"]
@property
def x(self):
return self._x
@x.setter
def x(self, value):
if not isinstance(value, int):
raise TypeError("unexpected type for x")
self._x = value
Comprobación de tipo de atributos
Ejemplos en la shell interactiva:
>>> Point(2, 3)
<__main__.Point object at 0x7f10fd76a150>
>>> Point(2.2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "x4_prop.py", line 16, in __init__
setattr(self, name, value)
File "x4_prop.py", line 29, in x
raise TypeError("unexpected type for x")
TypeError: unexpected type for x
Descriptores
Segunda aproximación para comprobar los tipos usando descriptores:
class Descriptor(object):
def __init__(self, name=None):
self.name = name
def __get__(self, instance, cls):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
Descriptores
class TypedDescriptor(Descriptor):
_type = None
def __set__(self, instance, value):
if not isinstance(value, self._type):
raise TypeError("unexpected type for {0}".format(self.name))
super().__set__(instance, value)
Descriptores
Aplicamos los descriptors a nuestra estructura de ejemplo:
class Integer(TypedDescriptor):
_type = int
class Point(Struct):
_fields = ["x", "y"]
x = Integer("x")
y = Integer("y")
Observaciones:
● _fields sirve para el constructor.
● Los descriptores reciben repetidamente el nombre del atributo.
● Obtenemos el mismo comportamiento que con properties.
Descriptores con Metaclases
Automatizamos ciertas partes de la creacion de clases de nuestras
estructuras en el proceso de su definición (compilación):
class MetaStruct(type):
def __prepare__(cls, *args, **kwargs):
return collections.OrderedDict()
def __new__(clst, name, bases, attrs):
params = []
param_type = Parameter.POSITIONAL_OR_KEYWORD
for name, attr in attrs.items():
if isinstance(attr, Descriptor):
params.append(Parameter(name, param_type))
attr.name = name
attrs = dict(attrs)
attrs["__signature__"] = Signature(params)
return super().__new__(clst, name, bases, attrs)
Descriptores con Metaclases
Automatizamos ciertas partes de la creacion de clases de nuestras
estructuras en el proceso de su definición (compilación):
class MetaStruct(type):
def __prepare__(cls, *args, **kwargs):
return collections.OrderedDict()
def __new__(clst, name, bases, attrs):
params = []
param_type = Parameter.POSITIONAL_OR_KEYWORD
for name, attr in attrs.items():
if isinstance(attr, Descriptor):
params.append(Parameter(name, param_type))
attr.name = name
attrs = dict(attrs)
attrs["__signature__"] = Signature(params)
return super().__new__(clst, name, bases, attrs)
Descriptores con Metaclases
Automatizamos ciertas partes de la creacion de clases de nuestras
estructuras en el proceso de su definición (compilación):
class MetaStruct(type):
def __prepare__(cls, *args, **kwargs):
return collections.OrderedDict()
def __new__(clst, name, bases, attrs):
params = []
param_type = Parameter.POSITIONAL_OR_KEYWORD
for name, attr in attrs.items():
if isinstance(attr, Descriptor):
params.append(Parameter(name, param_type))
attr.name = name
attrs = dict(attrs)
attrs["__signature__"] = Signature(params)
return super().__new__(clst, name, bases, attrs)
Descriptores con Metaclases
Ahora el constructor de la clase base de estructuras:
class Struct(object, metaclass=MetaStruct):
def __init__(self, *args, **kwargs):
bound_values = self.__signature__.bind(*args, **kwargs)
for name, value in bound_values.arguments.items():
setattr(self, name, value)
Descriptores con Metaclases
Y así queda la definición final de estructuras, sin atributos
innecesarios y sin repetir el nombre para los descriptores.:
class Point(Struct):
x = Integer()
y = Integer()
¿Como afecta todo esto al rendimiento?
Clase simple, con comprobación de tipos en el constructor:
python -m timeit -s "import x2_sim as s" "x = s.Point(2, 5)"
1000000 loops, best of 3: 1.01 usec per loop
Structura usando signaturas genericas y properties:
python -m timeit -s "import x4_prop as s" "x = s.Point(2, 5)"
10000 loops, best of 3: 61.6 usec per loop
Structura usando signaturas genericas y descriptores:
python -m timeit -s "import x5_desc as s" "x = s.Point(2, 5)"
10000 loops, best of 3: 64.8 usec per loop
Structura usando signaturas genericas, descriptores y metaclases:
python -m timeit -s "import x6_meta as s" "x = s.Point(2, 5)"
10000 loops, best of 3: 38.8 usec per loop
Generación dinamica código
Para evitar la constante de resolcion de herencia de los
descriptores, intentaremos generar en tiempo de definicion
(compilación) el codigo del setter y d
Comentarios de: Metaprogramación con Python 3 (0)
No hay comentarios