Python avanzado


Tras el post de Python básico viene el de Python avanzado. Sin embargo, la diferencia entre uno y otro es bastante grande. Advierto a los novatos que esta parte es mucho más difícil. Me he planteado hacer un "Python intermedio", pero creo que esa parte la dará la experiencia. Recomiendo practicar haciendo pequeños programas en Python antes de intentar abordar esta parte.

Si alguien tiene dudas, siempre puede volver a consultar el artículo de Python básico.

Espero haber cubierto suficiente materia como para que queden claros los conceptos principales.

Métodos mágicos

Existen algunos métodos que nos permiten alterar el comportamiento habitual de nuestras clases. Por ejemplo, está el método __getattr__, que se ejecutará cuando tratemos de acceder a un atributo que no exista y, por lo tanto, podremos alterar el comportamiento habitual que consiste en lanzar una excepción:

>>> class Example1(object):
...    pass
...
>>> class Example2(object):
...    def __getattr__(self, attr):
...       print 'Invalid attribute:', attr
...
>>> e1 = Example1()
>>> e1.whatever
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Example1' object has no attribute 'whatever'
>>> e2 = Example2()
>>> e2.whatever
Invalid attribute: whatever

Algunos de estos métodos mágicos son:

  • __getattr__ se ejecuta al acceder a un atributo inexistente.
  • __setattr__ se ejecuta al intentar modificar un atributo inexistente.
  • __delattr__ se ejecuta al eliminar el atributo.
  • __getattribute__se ejecuta antes de intentar acceder a cualquier atributo.
  • __new__ se llama antes de crear un objeto de la clase, cuando el objeto aún no existe.
  • __init__ se llama justo después de crear el objeto. Es el equivalente a un constructor.
  • __del__ se llama justo antes de destruir el objeto. Sin embargo, la destrucción de un objeto puede no realizarse al invocar al método del.
  • __repr__ debe devolver una representación del objeto, ya que se llamará por repr().
  • __str__ es una representación "informal" del objeto, y se llamará con str() o con print()
  • __lt__, __le__, __eq__, __ne__, __gt__, __ge__ permiten sobreescribir las comparaciones, aunque también se puede utilizar:
  • __cmp__, para realizar comparaciones
  • __hash__ debe devolver un identificador único para el objeto

Otros métodos mágicos nos sirven para permitir que nuestras clases se comporten como listas o diccionarios:

  • __len__ se consulta al ejecutar len()
  • __getitem__ llamado al evaluar self[key]
  • __setitem__ llamado al asignar a self[key]
  • __delitem__ llamado con del(self[key])
  • __iter__ solicita un iterador.
  • __reversed__ iterador inverso.

Hay muchos, MUCHOS métodos mágicos. Algunos son para secuencias, otros para contenedores, y otros para operaciones matemáticas. Podéis consultar la lista completa en el Python Data Model

Veamos ahora el uso práctico que pueden tener algunos de ellos:

Atributos bajo demanda

En python todo funciona como si fuera una tabla Hash o, en nomenclatura más pythónica, un diccionario. Existe un método mágico llamado __dict__, que es un atributo de sólo lectura que contine los valores del resto de atributos de un objeto.

Y podemos aprovecharnos de esta propiedad para hacer objetos que "mutan" de acuerdo a su inicialización:

class Example(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

El ejemplo actual crea una clase cuyos objetos contendrán como atributos cualquier cosa que se haya pasado como atributos nombrados:

>>> example = Example(var1=1, var2='hello')
>>> example.var1
1
>>> example.var2
'hello'
>>>

Diccionarios que se comportan como objetos

Otra característica que puede sernos de utilidad es tener un diccionario que se comporta como un objeto, es decir, que permite acceder a sus elementos como atributos. Esta característica puede hacer nuestro código más fácil de leer:

class DictObject(dict):
    def __getattr__(self, name):
        return self[name]

    def __setattr__(self, name, value):
        self[name] = value

Ahora podemos acceder al objeto como diccionario u objeto:

>>> example = DictObject()
>>> example['one'] = 'hello'
>>> example.one
'hello'
>>> example['one']
'hello'
>>>

Esto tiene una limitación: como los nombres de variables no pueden comenzar por números, contener espacios ni otros signos de puntuación, es posible que nunca podamos acceder a algunos valores:

>>> example = DictObject()
>>> example[1] = 'hello'
>>> example[1]
'hello'
>>> example.1
SyntaxError: invalid syntax
>>>

Objetos iguales

La igualdad entre objetos es relativa. En una ocasión necesité (haciendo un pequeño entorno de pruebas) un objeto que me permitiera insertar cualquier cosa. La solución habitual es ésta:

>>> ANYTHING = object()
>>> if a in (ANYTHING, 'hola'): pass # some stuff
>>>

Sin embargo hay otra forma más bonita de hacer las cosas:

>>> class MatchEverything(object):
...     def __cmp__(self, other):
...         return True
...
>>> ANYTHING = MatchEverything()
>>> if a == ANYTHING: pass # some stuff
>>>

En este segundo caso no es necesario comprobar si vale un valor concreto, ya que la propia comparación resolverá el problema. Limpio y sencillo.

Esta solución permite crear otro tipo de objetos interesantes, como StringContaining, PositiveNumber y otros objetos curiosos:

class StringContaining(object):
   def __init__(self, pattern):
       self._pattern = pattern

   def __cmp__(self, other):
      return self._pattern in other

called_me = StringContaining('MagMax')

if called_me('Hello, MagMax'):
    print('What do you want?')

Singleton

Estos métodos mágicos también pueden utilizarse para implementar el patrón Singleton. Este patrón es bastante controvertido, ya que las probabilidades de usarlo bien son más bien reducidas.

Es precisamente por esa razón que no lo explicaré aquí.

Decoradores

Existe la posibilidad de crear funciones que se ejecutarán con otras funciones. A esto se le denomina decorators. Tanto en Python como en Java comienzan por una arroba (@).

Hay distintas maneras de crear un decorator. La más sencilla es crear una función que devuelve una función:

def timed(method):
    def inner(*args, **kwargs):
        start = time.time()
        try:
            return method(*args, **kwargs)
        finally:
            print('ellapsed:', time.time() - start)
    return inner

@timed
def example(a):
    print(a)

example(5)

En este pequeño programa se crea un decorator llamado timed. Posteriormente se crea un método example empleando el decorator. La salida de la llamada final será:

>>> example(5)
5
ellapsed: 0.000157833099365
>>>

Conviene fijarse en ciertos aspectos:

  • No confío en el número de argumentos que puedan llegar al decorator. Habitualmente se procederá así.
  • No confío en que el método no lance excepciones, y las dejo pasar.
  • No interfiero en el resultado del método.

Los decoradores son muy potentes, pero hay que manejarlos con cabeza, ya que pueden tener demasiada "magia". Un ejemplo de lo que NO DEBE HACERSE BAJO NINGÚN CONCEPTO:

def catch_pokemon(method):
    def inner(*args, **kwargs):
        try:
            return method(*args, **kwargs)
        except:
            pass
    return inner

Estoy convencido que, si habéis llegado hasta aquí, sabéis por qué no debe hacerse. A este error se le suele denominar "catch pokémon", y cometerlo en un decorador puede ser terriblemente difícil de encontrar. Ya sabéis: un gran poder conlleva una gran responsabilidad.

@staticmethod y @classmethod

Hay algunos decoradores definidos en la librería estándar, como son @staticmethod y @classmethod. ¿Cuándo se debe usar cada uno de ellos?

El primero, @staticmethod, se utiliza para crear métodos que no requieren nada de la clase principal.

El segundo, @classmethod, recibirá la clase como primer argumento, por si se necesita utilizar para algo. Se suele llamar cls por convención.

Como norma general, no los utilicéis. He aquí una regla con los métodos estáticos: si tienes un método estático que llama a otro método estático, plantéate que puedes estar haciéndolo mal (habrá casos justificados, claro está).

Ambos tienen un uso muy interesante a la hora de crear Factories:

class MySqlConnection(object):
    # ...
class InMemmoryConnection(object):
    # ...
class DBConnection(object):
    @staticmethod
    def get_connection(type, *kwargs):
        types = {'mysql': MySqlConnection, 'memory': InMemoryConnection}
        clazz = types.get(type)
        if clazz is None:
            raise ValueError('Unknown database type')
        return clazz(**kwargs)

A veces también nos pueden valer para crear Builders, pero no debería ser lo habitual:

class Wheel(object):
    # ...
class Engine(object):
    # ...
class Car(object):
    # ...
class CarMaker(object):
    @staticmethod
    def create_seat_127():
        car = Car()
        for i in range(4):
            wheel = Wheel()
            # set here wheel parameters
            car.add(wheel)
        engine = Engine()
        # set here engine parameters
        car.add(engine)
        # ...
        return car

Propiedades

Hay lenguajes que animan a construir modelos anémicos, es decir, clases que sólo contienen getters y setters. En Python esto está completamente desaconsejado, ya que tenemos as properties.

Básicamente, una property consiste en un getter y/o setter que se maneja como un atributo. Esto permite transformar un atributo en una property en el momento en que es necesario, de manera que el uso siga siendo el mismo.

Veamos un ejemplo:

>>> class Example(object):
...     def __init__(self):
...         self.var = 2
...
>>> example = Example()
>>> example.var = 5
>>> example.var
5
>>>

Supongamos que tenemos la clase Example con el atributo var. Por requisitos del programa, queremos que var devuelva siempre el doble de su valor. Eso es un problema, ya que es un atributo... o no:

>>> class Example(object):
...     def __init__(self):
...         self._var = 2
...     def get_var(self):
...         return 2 * self._var
...     def set_var(self, value):
...         self._var = value
...     var = property(self.get_var, self.set_var)
...
>>> example = Example()
>>> example.var = 5
>>> example.var
10
>>>

Aquí han ocurrido varias cosas: lo primero hemos renombrado la variable var para que sea diferente. A menudo se empleará un underscore para indicar que es una variable privada (aunque en Python no hay nada privado realmente, es sólo notación). A continuación se crea la función con el getter y, finalmente, se crea la property con el nombre que tenía antes la variable.

Desde ese momento se ejecutará el getter cuando se intente acceder a la propiedad.

La función builtin property tiene los siguientes argumentos:

property(getter, setter, delete, doc)

De manera que nos permite sobreescribir el getter, setter, el destructor o la documentación.

Properties y decorators

Existe otra manera más sencilla de hacer lo mismo pero con decorators. Así, éste es el mismo ejemplo:

>>> class Example(object):
...     def __init__(self):
...         self._var = 2
...     @property
...     def var(self):
...         return 2 * self._var
...     @var.setter
...     def var(self, value):
...         self._var = value
...
>>> example = Example()
>>> example.var = 5
>>> example.var
10
>>>

Los métodos del decorador serían setter, getter y deleter, por lo que podemos hacer lo mismo que con property.

Generadores

Finalmente, y como concepto avanzado, aquí están los generadores. Consisten en funciones que producen elementos, pero la función queda en memoria para poder producir más elementos. Es el caso de la función range (en versiones anteriores a la 3.0 era xrange), que podría implementarse así:

def my_range(a, b=None, step=1):
    current = 0 if b is None else a
    end = a if b is None else b

    while current < end:
        yield current
        current += step

Veamos un ejemplo más sencillo aún: lo mismo, pero sin enrevesar los argumentos:

def my_range(start, end, step=1):
    current = start

    while current < end:
        yield current
        current += step

Como puede observarse, se utiliza la cláusula yield. Ésta es como un return, pero la función no termina. De esta manera se pueden recorrer todos los elementos. Puede haber elementos infinitos.

Esto, junto con algunos principios más, da lugar a la programación funcional.

With

Existe un elemento más muy curioso: with. A menudo resulta difícil de entender cuando es muy sencillo: simplemente invoca a los métodos __enter__ y __exit__ al comienzo y final del bloque, respectivamente. Esto da lugar a situaciones muy divertidas.

Por ejemplo, el método open abre un archivo, pero el objeto que devuelve soporta estas operaciones. Por eso podemos abrir un archivo de la siguiente manera, con la seguridad de que siempre se cerrará:

with open('filename') as f:
    print(f.read())

El equivalente (más o menos) sin el with sería algo así:

f = open('filename')
try:
    print(f.read())
finally:
    if f:
        f.close()

Como puede observarse, mucho más complejo.

Partials

A menudo es necesario escribir el mismo método varias veces cambiando únicamente un parámetro. En estos casos, podemos sobreescribirlo de una manera mucho más sencilla, creando una función igual pero con uno de los parámetros fijos. A esto es a lo que se conoce como partial method.

Esta utilidad se encuentra en el módulo functool. Veamos un ejemplo:

>>> def f(*args, **kwargs):
...     print args, kwargs
...
>>> f(5, a=10)
(5,) {'a': 10}
>>> import functools
>>> b = functools.partial(f, 5, a=10)
>>> b()
(5,) {'a': 10}
>>>

Como se ve en el ejemplo, b es una función parcial de f, en la que tenemos algunos métodos predefinidos. Es decir, sería equivalente a:

def b(*args, **kwargs):
   f(5, *args, a=10, **kwargs)

Archivos autoejecutables

Python dispone de un fuerte soporte para archivos comprimidos Zip. Podemos hacer la siguiente prueba: Escribid en un archivo llamado __main__.py la línea print "hello world". Ahora podemos comprimirlo con zip y tratamos de ejecutarlo:

$ cat __main__.py
print "hello, world"
$ zip example __main__.py
  adding: __main__.py (stored 0%)
$ python example.zip
hello, world
$

¡Magia! El archivo se ha ejecutado como si fuera un único archivo python.

Diversión

Python se desarrolla por la comunidad, y a la comunidad le gusta divertirse... Por eso hay escondidos algunos "Easter Eggs".

El primero de ellos sirve para mostrar "El Zen de Python". Basta con ejecutar:

$ python -m this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

El segundo surgió de una broma de XKCD, donde se mencionaba Python. Si veis la broma entenderéis por qué se lanza así:

$ python -m antigravity

Más información

El mejor sitio para obtener información básica de este lenguaje sigue siendo la propia documentación de Python.

Y vuelvo a recomendar los libros Dive into Python, de Mark Pilgrim, que es gratuíto y se puede descargar de esa misma dirección y también Pro Python, de Marty Alchin.


Comentarios

Comments powered by Disqus