Python Eficiente – Sobre la vida de los objetos

nov 09 2013

Antes de seguir adelante, necesitamos aclarar qué les pasa a los objetos que creamos en una aplicación. Cuándo se crean, dónde se almacenan y cómo se destruyen. En definitiva, necesitamos conocer mejor la vida de los objetos.

El término de variable que usamos en programación tiene su origen en el Álgebra Matemática. Una variable representa cada uno de los grados de libertad que tenemos, de forma que cambiando su valor obtendríamos diferentes resultados de una expresión.

Los primeros lenguajes imperativos, sobre todo BASIC, definieron las variables como espacios de memoria donde almacenar los distintos valores que necesitaba la CPU en sus operaciones. Cada variable se marcaba con un nombre único y se le asignaba un espacio en memoria. Con el fin de reducir el consumo de memoria, estas variables eran reutilizadas una y otra vez a lo largo del programa.

Variables de memoria

Con los lenguajes procedurales y lenguajes orientados a objetos se cambió este concepto. Los nombres de variables ya no eran únicos. Dos variables podían tener el mismo nombre en distintos ámbitos (scopes), así como dos variables podían representar el mismo dato. El nombre de la variable dejó de representar el espacio físico en memoria para convertirse en un alias con el que nombrar a la variable. El proceso de enlazar un nombre con un valor se llamó binding (enlace) y se hizo fundamental para el funcionamiento de las clausuras.

Se puede definir una variable como la “unión de un nombre y un valor a través de un enlace”.

Enlaces de nombres y objetos

Con este punto de vista, cuando hablamos de “modificar” una variable tenemos dos modos de hacerlo:

  • Modificando el valor al que apunta
  • Modificando el enlace para apunte a otro valor

Nosotros no sabemos, en realidad, cómo se modifican las variables. Lo único que nos tiene que importar es que nuestra variable modificada apuntará al nuevo valor. Así pues, cuando tenemos un código:

x = 12
y = x

Seguimos diciendo que “a la variable X le asignamos el valor entero 12″, pero lo correcto sería decir que “al entero 12 lo llamaremos X”. Y en lugar de decir que “a la variable Y le asignamos la variable X”, lo correcto sería decir que “la variable X también se va a llamar Y”. Pero la costumbre pesa más que la corrección.

Pensando en un lenguaje de programación como Python, donde todo son objetos, podemos ver nuestro entorno como un gran ecosistema poblado de objetos de todo tipo, que se crean, interaccionan y se destruyen. Al principio de una aplicación, sólo contamos con acceso a unos pocos objetos y nos las tenemos que apañar para acceder al resto de objetos a través de operaciones y llamadas a los distintos módulos disponibles. Nuestro espacio de nombres inicial se irá expandiendo progresivamente con las referencias de los objetos de nuestro mundo conocido.

Ciclo de la vida de un objeto

Lo primero que hay que tener claro es que en python no tenemos verdadero control sobre la creación y destrucción de los objetos. Sólo podemos asegurar que un objeto existe mientras haya una referencia que lo enlace. Para saber qué pasa, tendremos que indagar en el funcionamiento del intérprete python.

Objetos básicos

x = 2 + 3

En esta expresión, el intérprete emplea dos objetos existentes, ‘2‘ y ‘3‘, y obtiene un tercer objeto, ‘5‘, al que asigna el nombre de ‘x‘. El objeto ‘5‘ no sabemos si lo ha creado en el momento de evaluar la expresión o si ya existía.

Como optimización del intérprete, siempre están creados un conjunto de los objetos más comunes. Estos objetos son los números enteros desde -5 a 256 (incluido el 0), los booleanos True y False, None y los conjuntos vacíos inmutables (), frozenset() y "".

Para saber si dos objetos son el mismo, podemos usar la función id. Podemos decir que dos objetos son el mismo si la función id devuelve el mismo valor. Así, podríamos obtener fácilmente la lista de los números enteros que siempre tiene creados el intérprete:

[i for i,j in zip(range(-100,1000),range(-100,1000)) if id(i)==id(j)]

Internalización de cadenas

Más curiosas resultan las “internalizaciones” de las cadenas de caracteres. Para acelerar las búsquedas, el intérprete mantiene una tabla global interna con las palabras usadas en nombres de variables, funciones, módulos, etc. Adicionalmente, toda cadena de caracteres que usemos que cumpla con las reglas sintácticas para ser nombres de variables acabarán automácamente dentro de esta tabla interna.

Además de este funcionamiento automático, podemos forzar a que una cadena entre en esta tabla con la función intern (sys.intern en python3).

Pues bien, todas las cadenas de caracteres de la tabla interna sólo son creadas una vez durante toda la ejecución del programa y permacerán ahí hasta el final.

>>> a="hola"
>>> b="hola"
>>> id(a)==id(b)
True
>>> a="hola mundo"
>>> b="hola mundo"
>>> id(a)==id(b)
False
>>> a=intern("hola mundo")
>>> b=intern("hola mundo")
>>> id(a)==id(b)
True

No siempre funciona el mecanismo de internalización y el intérprete crea una nueva cadena de caracteres:

>>> a="hola"
>>> c="HOLA".lower()
>>> id(a)==id(c)
False
>>> c=intern("HOLA".lower())
>>> id(a)==id(c)
True

Por culpa de la internalización nunca podremos estar seguros de cuándo se crea una cadena de caracteres. Más allá de este hecho, nunca nos debería preocupar el internalizar o no las cadenas de caracteres que usemos. Al menos, yo no he encontrado ninguna ventaja concreta de hacerlo.

Asignaciones

Ya hemos comentado que una asignación directa no crea un objeto nuevo, si no que enlaza una nueva etiqueta con el objeto existente:

x = y

En este caso, el mismo objeto al que apunta ‘y‘ también será al que apunte ‘x‘. Muchas veces no querremos que esto ocurra, sobre todo en el caso de listas. El truco consiste en convertir la asignación directa en una expresión que cree un nuevo objeto, pero de igual valor. Para ello usaremos las operaciones idempotentes para cada tipo de dato:

Para números en general, podemos usar los elementos neutros de las operaciones y*1 ó y+0:

>>> x=2.0
>>> y=x
>>> id(y)==id(x)
True
>>> y=x*1
>>> id(y)==id(x)
False
>>> y==x
True

En el caso de listas, por convenio se suele usar el operador split lista[:], pero podríamos usar cualquier otro como lista*1 o lista+[].

Destrucción de un objeto

Saber cuándo acaba la vida de un objeto suele ser la parte que más despista a quienes vienen a python desde otros lenguajes donde se acostumbra a hacer desaparecer un objeto por la fuerza.

Una vez más: en python, un objeto existe mientras esté referenciado.

Sólo cuando desaparezca la última referencia al objeto se llamará a su destructor (método __del__) y será eliminado de memoria.

No nos preocupamos de ello, pero cuando finaliza la ejecución de una función o de un método, desaparecen todas las referencias que habíamos creado. No hace falta que lo hagamos explícitamente. Todos los objetos creados durante la ejecución dejan de estar referenciados y serán destruidos, con excepción de aquellos que se retornen como resultado.

Pero hay veces que guardamos referencias a objetos que ya no nos hacen falta, y no somos muy conscientes de que por culpa de estas referencias estos objetos no son destruídos. Por ejemplo, es frecuente ver aplicaciones que mantienen una lista de ventanas abiertas. Por culpa de esta lista, las ventanas siempre estarán referencias. Si en el destructor estaba el código para eliminar la ventana y sus componentes, resulta que nunca será llamado. Hace falta eliminar la referencia de la lista de ventanas para que la ventana sea destruida finalmente.

En próximos artículos veremos técnicas mejores, como son usar “referencias débiles” (weakrefs). Las weakrefs vienen a ser referencias a objetos que no obligan a que el objeto esté siempre vivo. Si todas las referencias un objeto son weakrefs, entonces el objeto podrá ser destruído.

Referencias circulares

Algunas veces, los objetos mantienen referencias entre ellos conocidas por “referencias circulares”:

>>> a=[]
>>> b=[a]
>>> a.append(b)
>>> a
[[[...]]]

De querer eliminar ambos objetos, no podríamos hacer nada al estar referenciados mutuamente. Para estos casos, el intérprete de python tiene un proceso propio que se dedica a detectar estas referencias circulares llamado “Recolector de Basura”, más conocido por sus siglas GC (Garbage Collector). GC es un proceso que está permanentemente explorando la memoria para mantenerla limpia de objetos innecesarios, siendo parte vital para el correcto funcionamiento del intérprete. (Más información, en la documentación del módulo gc).

5 responses so far

Python Eficiente – Hacia la programación funcional

oct 26 2013

Existen muchas definiciones de Programación Funcional, así como comparativas con otros paradigmas de la programación que más parece una cuestión de gustos que una visión razonada de ventajas e incovenientes. No voy a entrar en definiciones tediosas que necesitan demasiadas explicaciones. Prefiero verlo más como si se tratara de una confrontación entre ingenieros y matemáticos.

Un ingeniero piensa más en números que hay que traer de una zona de memoria, operar con ellos y luego almacenar hasta la siguiente operación. Un matemático es más de dejar las operaciones para el final. Asigna a cada número una letra, al resultado lo llama incognita, e intenta operar algebraicamente. Así, a grosso modo, estaríamos hablando de programación imperativa versus programación funcional, respectivamente.

Para lo que nos interesa en python, la visión de retrasar las operaciones hasta el final es la característica de la programación funcional que más nos puede interesar. Nos va a permitir crear código más eficiente, además de enfocar algunos problemas desde un punto completamente diferente, más matemático si cabe.

Podría haber llamado a esta serie de artículos “Python Funcional”, pero es casi seguro que mucha gente hubiera pasado de ellos por considerar esta temática un rollazo, radicalmente distinta de cómo se enseña la programación hoy en día (a mi juicio, equivocada).

En cambio, llamándolos “Python Eficiente” seguro que a más de uno le pica la curiosidad. En realidad, mi objetivo no es sólo hablar de programación funcional. También tratará de crear código que consuma menos recursos tales como tiempo de CPU o memoria. En definitiva, que problemas que creíamos fuera de nuestro alcance, podamos resolverlos con nuestros humildes medios.

No te pierdas los próximos artículos.

No responses yet

Clausuras en python – Parte 2

oct 26 2013

Ámbitos anidados

La importancia de disponer de clausuras va más allá de saber dónde se evalúa la función. Si fuera posible encapsular una función junto con su propio entorno de ejecución, podríamos conseguir que la función tenga “memoria” o, dicho de otro modo, que sea capaz de conservar sus propios estados entre llamadas a la función. Este empaquetado de función y entorno de ejecución se denomina a veces clausuras verdaderas y suele ser la principal característica de los llamados Lenguajes Funcionales.

En python podemos crear estas clausuras verdaderas con **funciones anidadas*, donde una función está definida dentro del ámbito de la otra.

Un ejemplo sencillo:

def incr(n):
    def aux(x):
        return x+n
    return aux

inc5=incr(5)

print inc5(10) #-->15

Como resultado se devuelve la función aux, definida dentro del ámbito de incr y que emplea de éste la variable n. Internamente, se conserva la referencia a la variable n, pero no será accesible desde fuera de la función aux. Hemos podido empaquetar la función junto con el entorno donde se definió.

Pongamos otro ejemplo:

def count():
    num=0
    def aux():
        num+=1
        return num
    return aux

c1=count()

c1()  #--> 1
c1()  #--> 2
c1()  #--> 3

Si pruebas este código te dará error. La función anidada aux intenta modificar la variable num. Para este caso, la variable se crea dentro del ámbito más interno, en lugar de usar la variable disponible. Y como se intenta modificar la variable antes de asignarle un valor, entonces se produce el error.

Como solución, podríamos hacer la variable num global para que fuera accesible por todos los ámbitos. Pero esta solución no es buena ya que nos abriría el empaquetado. Para python3 podríamos declarar la variable como nonlocal para que se busque en los ámbitos superiores:

def count():
    num=0
    def aux():
        nonlocal num
        num+=1
        return num
    return aux

Como solución para salir del paso, se puede evitar la reasignación de variables. Por ejemplo, usando una lista:

def count():
    num=[0]
    def aux():
        num[0]+=1
        return num[0]
    return aux

Ya sé que no es muy elegante, pero hay otras formas de hacerlo mejor.

Generadores

Una de las formas más comunes de usar clausuras es a través de generadores. Básicamente, son funciones que en lugar de usar return utilizan yield para devolver un valor. Entre invocaciones, se conserva el entorno de ejecución y continúan desde el punto desde donde estaba. Para el ejemplo anterior:

def count():
    num=0
    while True:
        num+=1
        yield num

c1=count()
next(c1) #-->1
next(c1) #-->2

Objetos funciones

En los ejemplos que hemos visto, podríamos tener varias clausuras de una misma función. Si hemos hecho bien las tareas, la ejecución de estas clausuras son independientes:

c1=count()
c2=count()

next(c1) #-->1
next(c1) #-->2
next(c2) #-->1
next(c2) #-->2
next(c1) #-->3

Con ello es posible establecer una analogía con clases y objetos. La definición de la función sería la clase y la clausura la instancia de la clase.

¿Y si lo hacemos posible? En python se denominan callables a todo objeto que tenga un método __call__, comportándose como si fueran funciones (Functores). Contruyamos una callable que funcione como una función con clausura:

class Count(object):
    def __init__(self):
        self.num=0
    def __call__(self):
        self.num+=1
        return self.num

c1=Count()
c1() #-->1
c1() #-->2
c1() #-->3

Sin duda es la manera más elegante de usar clausuras que tenemos en python. Evita muchos problemas y nos da una gran potencia a la hora de resolver algunos problemas.

Por ejemplo: imagina que queremos recorrer una lista de números, excluyendo los que sean pares, y siempre que la suma total de los números que ya hemos visitado no supere cierto límite.

En una primera aproximación se podría crear un generador:

def recorr(lista, maximo):
    total=0
    for i in lista:
        if i%2!=0:
            if total+i<maximo:
                total+=i
                yield i
            else:
                break

recorr([3,6,7,8,11,23],30) #-->[3,7,11]

Está bien, pero no es fácil de usar. Aunque sólo necesitemos algunos elementos, seguramente estemos obligados a crear una lista completa con todos los valores1. Encima, no tenemos acceso a la variable total para saber cuánto han sumado el resultado.

Una alternativa con objetos funciones, mucho más elegante:

class RecorrFunc(object):
    def __init__(self, maximo):
        self.maximo=maximo
        self.total=0
    def filter(self, item):
        res= item%2!=0 and self.total+item<self.maximo
        if res:
            self.total+=item
        return res
    def __call__(self, lista):
        return [x for x in lista if self.filter(x)]

recorr=RecorrFunc(30)
recorr([3,6,7,8,11,23]) #-->[3,7,11]
print recorr.total #-->21

Las posibilidades de los objetos función son muchas. Del mismo modo que se devuelve una lista, sería posible devolver un iterador. Empleando las funciones del módulo itertools, y algunos trucos más, podríamos aplicar los principios de la programación funcional en python sin problemas.

Pero éso lo veremos en próximos artículos.


  1. No sabemos de antemano cuántos items vamos a obtener. Si, por ejemplo, necesitamos sólo los tres primeros, tendremos que iterar elemento a elemento hasta llegar a los tres que necesitamos o, bien, hasta que quede exhausto el iterador. Con la solución con funtores el proceso es mucho más directo y eficiente. 

3 responses so far

Clausuras en python – Parte 1

oct 25 2013

Funciones Lambda

Antes de ver qué son las clausuras (closures), veamos qué tienen las funciones lambda que las hacen tan polémicas algunas veces.

Comencemos con un ejemplo. Te recomiendo que te esfuerces en deducir cómo funciona sin ir a probar cómo funciona. A continuación te pondré algunos valores para que elijas los valores de las tres listas:

    i=1
    add_one=lambda x:x+i
   
    lista1=[add_one(i) for i in [0,1,2]]

    i=0
    lista2=[add_one(i) for i in [0,1,2]]

    i=2
    lista3=[add_one(i+1) for i in [0,1,2]]

Valores para lista1:

  1. [0,1,2]
  2. [1,2,3]
  3. [0,2,4]
  4. [1,3,5]

Valores para lista2:

  1. [0,1,2]
  2. [1,2,3]
  3. [0,2,4]
  4. [1,3,5]

Valores para lista3:

  1. [0,1,2]
  2. [1,2,3]
  3. [2,3,4]
  4. [1,3,5]

Las soluciones están al final del artículo1, pero puedes probarlo ahora para que lo veas tú mismo.

¿Qué es lo que ha pasado?

Contrariamente a lo que estamos acostrumbrados con las funciones normales, la evaluación de una función lambda se hace dentro del entorno donde se ejecuta, independiente del entorno donde se ha definido. Así pués, en la función lambda lambda x:x+i, la variable i toma el valor de esta variable en el momento de evaluar la función. Como se usa esta variable para la compresión de la lista, irá cambiando de valor a medida que se recorre la lista [0,1,2], por lo que la expresión add_one(i) termina convirtiéndose en la expresión i+i, y la expresión add_one(i+1) en i+1+i.

Tiene un funcionamiento similar a los macros, donde se sustituye literalmente la llamada a la función por la expresión equivalente. En python3, se hace más evidente al denominarse expresiones lambda en lugar de funciones lambda.

Clausuras

En una función podemos distinguir dos partes:

  • Código ejecutable
  • Entorno de evaluación, más conocido por Ámbito o Scope

Antes de ejecutar el código de la función, se aumenta el entorno de evaluación con los argumentos de entrada de la función.

Según en qué entorno se evalua la función, tenemos dos ámbitos:

  • Clausura, también llamado Ámbito léxico o Ámbito Estático, cuando la función se evalua en el entorno donde se ha definido.
  • Ámbito dinámico cuando se evalua en el entorno donde se invoca la función.

Con esta definición, podemos afirmar que en python las funciones tienen ámbito léxico, con excepción de las funciones lambda que tienen ámbito dinámico.

No voy a considerar las ventajas de uno u otro tipo. Por lo general, las clausuras se consideran mejores para desacoplar el código de la función del código donde se invoca, lo que ayuda mucho al mantenimiento y corrección de errores. Es por ello la manera normal de crear funciones en la mayoría de lenguajes de programación.

¿Cómo hacer que una función lambda se comporte como si tuviera clausura?

La forma de hacer que un función lambda se evalue en el entorno donde se define consiste en pasar las variables de ese entorno que necesite en los argumentos de entrada, casi siempre como argumentos por defecto.

En el ejemplo anterior sería:

    i=1
    add_one=lambda x,i=i:x+i

que equivaldrá a

    add_one=lambda x,i=1:x+i

En este caso i se toma de los argumentos de la función, y tendrá por defecto el valor de i en el momento de la definición de la función lambda.

No es perfecto, pero es lo mejor que tenemos. Lo recomendable es evitar las funciones lambda complejas si no queremos llevarnos algunas sorpresas.


  1. Los valores de las listas son las opciones 3, 3 y 4, respectivamente. 

No responses yet

Nuevos planteamientos, nueva etapa

oct 07 2013

Si me sigues por twitter, sabrás que he cambiado de trabajo. Aunque sigo siendo “informático” a todos los efectos, para mi “core” interno ha supuesto un cambio radical de trabajo. Mi labor actual está orientada más a temas de diseño y gestión de sistemas y telecomunicaciones, lo que relega las tareas de programación a un nivel secundario.

No espero que este cambio me suponga abandonar el estudio de nuevos conceptos y metodologías de la programación, como hacía hasta ahora. Tal vez, sí que suponga que en este blog incluya temas de gestión de sistemas que hasta ahora no incluía, aunque todavía no tengo claro en qué medida puedo aportar algo que sea original en esta temática.

Lo que sí que me estoy planteando reducir la programación en python. No es por ningún motivo especial, si no por el deseo de explorar otros mundos como son la programación reactiva con scala y el análisis de datos con R. No tener que programar en el trabajo supone una liberación para hacer este viaje, casi como si me hubieran concedido un periodo sabático.

Aún tengo en el cajón varios artículos sobre python que iré publicando progresivametne, y pienso seguir cualquier novedad que surga. Lo que quizás se note más sea que bajaré mi actividad (que no desaparición) en los distintos foros python. Posiblemente, haya muchos que ni se den cuenta.

4 responses so far

Estudio función factorial – numpy

ago 14 2013

Mientras busco tiempo para preparar algunos artículos sobre cómo hacer la programación python más eficiente, he estado revisando nuevos métodos de programar la función factorial en python aplicando los nuevos conocimientos adquiridos.

Como puse en un artículo anterior, la implementación más compacta de la función factorial sería aplicando la función reduce:

def fact(n):
    return reduce(lambda x,y:x*y, xrange(2,n+1), 1)

…o usando el operator.__mul__:

from operator import __mul__

def fact(n):
    return reduce(__mul__, xrange(2,n+1), 1)

También contaba el caso de una compresión de listas “bizarra” que evitaba el uso de reduce y lambda:

def fact(n):
    return [j for j in [1] for i in xrange(2,n+1) for j in [j*i]][-1]

El problema con esta expresión es que calcula todos los elementos de la lista para quedarse únicamente con el último elemento. Una forma de hacer lo mismo, sabiendo que la función factorial es estrictamente creciente, es obteniendo el máximo con max:

def fact(n):
    return max(j for j in [1] for i in xrange(2,n+1) for j in [j*i])

Para este tipo de tareas, en las que tenemos un iterador y queremos quedarnos con el último elemento, resulta mucho más eficiente el uso de la colección deque limitando el número de elementos de la lista:

from collections import deque

def fact(n):
    return deque((j for j in [1] for i in xrange(2,n+1) for j in [j*i]), maxlen=1)[0]

Por comparar tiempos, para el cómputo de fact(10000) me salen estos tiempos:

reduce+lambda        72.0 ms
reduce+operator      71.2 ms
comprensión listas  173.0 ms
función max          75.1 ms
deque                75.5 ms

Como se puede apreciar que los tiempos son muy similares (con la excepción de la compresión de listas debido a su gasto de memoria). Es lógico suponer que donde más tiempo se gasta es el cómputo de la multiplicación con la precisión absoluta que tienen los números longs de python.

De hecho, no se consigue gran cosa usando las librerías de cálculo numérico más conocidas de python. Se hace imposible optimizar nada sin pérdida de precisión o que salgan resultados extraños. Aún así, podemos expresar formas muy compactas para expresar la función factorial en numpy si forzamos en el uso del tipo object para que así no lo optimice:

import numpy as np

def fact(n):
    return np.arange(2,n+1,dtype=object).prod()

Tarda 70.8 ms. en calcular fact(10000), que es similar al resto de funciones factoriales que hemos visto. Da una buena idea de lo bien optimizada que está la librería numpy para cualquier cosa, incluso impidiéndole que optimice los tipos de datos que emplea.

4 responses so far

La Odisea

jul 29 2013

Puede ser raro que alguien de ciencias, como yo, escriba sobre una de las obras más significativas de la literatura griega clásica. Pero, contrariamente a la gente de letras que sabe distinguir qué es o no de letras, la mentalidad científica ve ciencia por doquier, incluso en la Odisea.

Recientemente he visto por televisión la película La Odisea de Andréi Konchalovski, producida por Francis Ford Coppola, una adaptación libre de la “Ilíada” de Homero con añadidos de la “Eneida” de Virgilio (el archiconocido caballo de Troya). Si bien como película de fantasía y acción se trata de un producto aceptable, sus conclusiones finales me han decepcionado bastante.

La trama de la película se centra en el castigo de los dioses al orgulloso Ulises, conquistador de Troya. Después de muchos años vagando por todo los mundos conocidos e inframundo, finalmente se somete a la voluntad divina, quienes le permiten regresar a Ítaca para recuperar su “mundo”, defender a su familia y acabar con todos los que pretendían ursurpar su reino. Un planteamiento demasiado “conservador” de la voluntad humana que desvirtúa bastante el verdadero espíritu de la Ilíada.

Las aventuras de Ulises deberían verse en realidad como un viaje hacia el conocimiento. Allá donde recala, Ulises adquiere más sabiduría y conocimientos. Aquél hombre inteligente que derrotó a los troyanos, acaba volviendo sabio después de muchos años. Definitivamente Ítaca no era el destino del viaje, si no la Sabiduría.

En la película, Ulises decide viajar al reino de Hades para que el profeta ciego Tiresias le indique cómo volver a Ítaca. Tiresias le dice que el ciego es él por no reconocer en el cielo estrellado el modo de regresar a casa: “La constelación Orión siempre al acecho de la Osa, también llamada carro, la única constelación que nunca se baña en el mar. Mantén siempre la Osa a tu izquierda y llegarás a Ítaca”. Tiresias le da el conocimiento para no depender de los dioses, del mismo modo que Prometeo entregó el fuego del conocimiento a los hombres. Con este conocimiento, Ulises es capaz de volver a casa como hombre sabio.

Realmente, en la Ilíada no es Tiresias quién indica el camino a casa, si no la divina Calypso cuando Ulises es finalmente perdonado por los dioses. Pero aparte de esta licencia para lucimiento de Christopher Lee en el papel de Tiresias, hay algo en esta indicación que resulta raro para una mente científica. Situada sobre el eje de giro de la Tierra, la Osa Mayor gira sobre sí misma. Dependiendo de la latitud, esta constelación nunca se pondría en el horizonte marino de modo que resulta una buena referencia para guiarse por la noche. Pero, ¿por qué no se habla de la estrella Polar como indicadora del norte? Muy sencillo: en la época en la que se escribió la Ilíada, la estrella polar no estaba fija en el cielo, giraba con el resto de estrellas de la constelación debido a que la inclinación del eje de la Tierra cambia con el tiempo. En la actualidad, la estrella Polar coincide con el eje de giro, pero no siempre ha sido ni será así.

Es el conocimiento lo que finalmente nos libera del yugo de los dioses. Tal y como cuenta en su poema “Ítaca” el griego Constantinos P. Cavafis:

    Cuando salgas de viaje para Ítaca
    desea que el camino sea largo,
    colmado de aventuras, colmado de experiencias.
    A los lestrigones y a los cíclopes,
    al irascible Poseidón no temas,
    pues nunca encuentros tales tendrás en tu camino,
    si tu pensamiento se mantiene alto,
    si una exquisita emoción te toca cuerpo y alma.
    A los lestrigones y a los cíclopes,
    al fiero Poseidón no encontrarás,
    a no ser que los lleves ya en tu alma,
    a no ser que tu alma los ponga en pie ante ti.

    Desea que el camino sea largo;
    que sean muchas las mañanas estivales
    en que entres en puertos que ves por vez primera.
    Detente en los mercados fenicios,
    adquiere sus bellas mercancías,
    madreperlas y nácares, ébanos y ámbares,
    y voluptuosos perfumes de todas las clases,
    todos los voluptuosos perfumes que te sean posibles.
    Y vete a muchas ciudades de Egipto,
    y aprende, aprende de los sabios.

    Mantén siempre a Ítaca en tu mente.
    Llegar allí es tu destino.
    Pero no tengas la menor prisa en tu viaje.
    Es mejor que dure muchos años
    y que viejo, al fin, arribes a la isla,
    rico por todas las ganancias de tu viaje,
    sin esperar que Ítaca te vaya a ofrecer riquezas.

    Ítaca te ha dado un viaje hermoso.
    Sin ella no te habrías puesto en marcha.
    Pero no tiene ya más que ofrecerte.

    Aunque la encuentres pobre, Ítaca de ti no se ha burlado.
    Convertido en sabio y con tanta experiencia,
    ya habrás comprendido el significado de todas las Ítacas.


Desde la primera vez que escuché este poema en voz de Bernardo Souvirón fui consciente que lo importante de este viaje hacia Ítaca, que todos estamos realizando, no es el llegar pronto, si no la riqueza de los conocimientos que atesoramos por el camino. No tengas prisa.


PD: por cierto, recomiendo seguir a Bernardo Souvirón, profesor de lenguas clásicas, en sus intervenciones de radio RNE y diversos libros. Si piensas que la historia clásica es un rollo es porque nunca las has oído contar de la manera que las cuentas Bernardo Souvirón.

2 responses so far

Borrado de un descriptor (corrección de errores)

jul 13 2013

Tengo que hacer algunas correcciones a la serie de artículos sobre descriptores, en concreto sobre el método __delete__ del protocolo descriptor.

Primero, aclaremos cómo funciona el método __delete__ y en qué se diferencia de __del__. No se trata de métodos destructores tal y como se entiende en otros lenguajes de programación orientados a objeto. En python, todo objeto está vivo mientras esté referenciado. Sólo cuando se pierda la última referencia se procederá a la destrucción y borrado del objeto en memoria por parte del recolector de basura.

Por ejemplo, veamos el siguiente código:

class Miclase(object):

    def __del__(self):
        print "instance deleted"
   
a = Miclase()
b = a
del a

print b
print "Come on"
b=1
print "END"

De su ejecución, podemos comprobar que el método __del__ no se invoca justo en el momento de hacer del a, si no cuando se pierde la última referencia al asignar otro valor a la variable b. La sentencia del a no destruye el objeto, tan sólo desliga el objeto de la etiqueta a que lo referenciaba. Por ese motivo, es inexacto hablar en python de “variable de memoria”, como se entiende en otro lenguajes. Tan sólo cambiamos de una referencia de un objeto a otro, sin destruir su valor anterior.

Revisión del protocolo descriptor

En un anterior artículo distinguía entre descriptores de datos y de no-datos. Hay que aclarar que un descriptor de datos “es también el que sólo tiene definido un método __delete__, aunque no tenga método __set__“. ¿Para qué puede sernos útil tener uno sin el otro?

Un descriptor de datos sin método __set__ no tiene forma de impedir que el atributo/método que implementa sea sustituído por otro objeto (por ejemplo, por otro descriptor). El método __delete__ nos daría la última opción de liberar recursos que ya no vayamos a usar antes de desaparecer el descriptor. Pero, independiemente de lo que haga, el método __delete__ indicaría que el descriptor puede ser sustituido. En definitiva, se comportaría como un descriptor de no-datos, pero con las diferencias en la invocación entre estos dos tipos de descriptor1.

Para aclarar las cosas, veamos qué estaba mal en el ejemplo que puse en su momento sobre el uso de __delete__ (he cambiado algunos nombres para que se vea más claro):

class Desc(object):

    def __init__(self, mul):
        self.mul=mul

    def __get__(self, obj, cls=None):
        return obj.value*self.mul

    def __set__(self, obj, value):
        raise AttributeError

    def __delete__(self, obj):
        del self

class Miclase(object):

    a12=Desc(12)
    a200=Desc(200)
   
    def __init__(self,value):
        self.value=value


c=Miclase(2)

print c.a12 #--> 24

c.a12=100 #ERROR: AttributeError

del Miclase.a12
c.a12=100

print c.a12  #--> 100 (no descriptor)

La idea era que se pudiera borrar el descriptor de datos para sustuirlo por otro valor. Tal como señalaba Cristian en un comentario al respecto, este ejemplo parece funcionar con o sin el método __delete__ en el descriptor.

Funciona siempre debido a que con 'del Miclase.a12' estamos borrando la referencia al descriptor que tiene la clase, sin pasar por el protocolo descriptor. La particularidad de los descriptores es que viven en la clase, pero se invocan desde la instancia. Con 'del Miclase.a12' estamos saltándonos el protocolo descriptor para acceder directamente al atributo de la clase2.

Además, este código no funcionaría nunca:

    def __delete__(self, obj):
        del self

Si la idea era borrar el objeto self, referencia al descriptor, podemos quitarnos esa idea ya que el comando del borra la referencia del scope local donde se encuentra. ¡No es un destructor! En realidad, todas las variables locales son borradas al finalizar el método. En este caso en concreto, también la variable local obj será borrada aunque no se indique explícitamente.

Otra cuestión a tener en cuenta es que los atributos de clase son compartidos por todas sus instancias. Si en algún momento alteramos un descriptor (por ejemplo, borrándolo), entonces todas las instancias sufririan el mismo cambio. No parece que sea el efecto buscado.

La gran pregunta es entonces, ¿cómo podemos aprovecharnos del método __delete__?

Para sacarle algún partido, el descriptor debería comportarse de forma distinta según sea la instancia que lo invoca. Definido así el descriptoor, entonces podríamos usar el método __delete__ para simular el borrado del atributo para esa instancia, sin que el descriptor pierda su funcionalidad.

Un ejemplo para ilustrar ésto sería:

from weakref import WeakKeyDictionary

class Desc(object):

    def __init__(self):
        self.data = WeakKeyDictionary()

    def __get__(self, obj, cls=None):
        if obj not in self.data:
            raise AttributeError
        total = sum(x for x in self.data.values())
        return (self.data.get(obj), total)

    def __set__(self, obj, value):
        if obj in self.data:
            raise AttributeError
        self.data[obj] = value

    def __delete__(self, obj):
        del self.data[obj]

class Miclase(object):

    value = Desc()


a = Miclase()
b = Miclase()

a.value = 2
b.value = 5

print a.value  #--> (2, 7)
print b.value  #--> (5, 7)

a.value = 100 #ERROR: AttributeError

del a.value
a.value = 11

print a.value  #--> (11, 16)
print b.value  #--> (5, 16)

del b

print a.value  #--> (11, 11)

El descriptor mantiene un diccionario weak con valores asignados para cada instancia de la clase. Usamos para ello un WeakKeyDictionary que tiene la particularidad de relajar la referencia al objeto, de modo que si todas las referencias al objeto son borradas en el programa, también es borrada la referencia que conservaba el diccionario.

En este ejemplo, el método __get__ devuelve el valor del atributo si el objeto está en el diccionario, si no da error. El método __set__ asigna un valor al atributo sólo si el objeto no existe. Para ver mejor el funcionamiento, el método __get__ devuelve una tupla con el valor del atributo y la suma de todos los atributos.

Ejecuntado el ejemplo, creamos dos instancias y les asignamos un valor al atributo controlado por el descriptor. Una vez asignado un valor, ya no podemos cambiarlo. La única opción será borrar el atributo y volverlo a asignar.

También se puede comprobar que, cuando borramos el objeto b, la suma de todos los atributos se actualiza a las instancias que aún quedan vivas.

En el borrado del atributo se usa el método __delete__ del descriptor; en el borrado de la instancia, el método __del__ (si existiera).

Referencia

No quisiera acabar este artículo sin añadir una referencia sobre este tema que os recomiendo leer, con algunas recetas para aprovechar el uso de los descriptores:

“Python Descriptors Demystified” by Chris Beaumont


  1. Comentado en los anteriores artículos sobre descriptores

  2. Un modo de impedir el borrado de atributos de una clase sería aplicando el protocolo descriptor con metaclases, pero pienso que estaríamos complicándolo todo demasiado para el beneficio que pudiera obtenerse a cambio. 

4 responses so far

Balance y cierre de ejercicio

jun 22 2013

Ya sé que parece extraño cerrar un periodo en mitad de año, pero los que me siguen de aquí a unos años sabrán que suelo hacer un breve resumen de situación comentando qué he estado haciendo y qué proyectos futuros estoy ideando.

Como puse en el último twitt, parece que voy a cambiar de trabajo. Después de 20 años dedicados a la sanidad pública, pasaré a trabajar para la gestión informática de la educación pública. Ya sé que son dos de los sectores públicos más castigados por los recortes presupuestarios, pero al menos queda algo, no como en el resto de departamentos.

Lo primero de todo es volver a afirmar una vez más que no voy a dejar python. Agradezco los mensajes de ánimo para que continúe participando en la comunidad hispana de python, incluso alguna que otra oferta para trabajar en proyectos punteros. No voy a abandonar python, tan sólo se me ha quedado pequeño. Ahora cierro el año que me dí para hacerme experto programador en scala y este año que viene haré lo propio con R.

Hace varios años que no programaba nada en python para el trabajo. En realidad, mi labor ha consistido en mantener y terminar proyectos que dejaban inconclusos las empresas externas que iban quebrando y que dejaban todo sin documentar y sin metodología alguna. Ha sido mucho código “desestructurado” el que he tenido que leer y entender. Lo peor es que la actual situación económica de la administración pública no permite configurar un equipo de desarrolladores capaces de abarcar estos proyectos y, peor áun, si algún día salimos de la crisis y la administración pública vuelve a disponer de medios económicos, en primer lugar no habrá gente capacitada (habrán huido del pais si son inteligentes). Lo segundo, los dirigentes políticos no confían en sus funcionarios y optan por sacar estas funciones del control público a empresas externas que puedan manipular y de las que puedan beneficiarse en exclusiva.

Porque hay que decirlo bien claro: los datos de la administración pública deben ser públicos. No valen excusas para no publicar las listas de esperas de sanidad, ni saber cuántos contratos se hacen al mes, ni cuánto se cobran de dietas,… Como informáticos, conocemos que toda esta información se está introduciendo al día en el sistema, casi en tiempo real. El gran problema es que un político no cree en los datos, sólo cree en aquello que confirme lo que cree. Como leí hace poco en un libro1: “El propósito último del análisis de datos es convencer a otras personas que sus creencias pueden ser alteradas por los datos”. Y creo que es aquí donde los informáticos somos más temidos. Nos niegan ser parte de las juntas de dirección porque nuestros razonamientos se basarían en datos reales gracias a nuestro conocimiento integral de la estructura de la empresa. Somos demasiado poco manipulables para ser directivos.

Hay que exigir que la administración pública abra sus datos. Son de agradecer los primeros esfuerzos en opendata que realizan algunos gobiernos autonómicos; pero son datos muy escasos y limitados, casi ridículos en comparación con la cantidad total de datos que gestiona. Debería exigirse, por ley, que toda empresa que trabaje para la administración pública publique sus datos. Ya no sólo porque los ciudadanos queramos saber más, si no porque el dinero público debe beneficiar a todos, incluso a las empresas que no conseguieron el contrato y que quieran mejorar.

Padezco de cierto Síndrome de Casandra, pero si me preguntaran cómo pienso que será el futuro, imagino en un mundo inhundado de datos abiertos. La huella que dejaron generaciones pasadas en este océano de datos serían la base con la que investigadores del presente harían nuevos descubrimientos médicos y científicos. Datos que contradigan la manipulación del presente por políticos y multinacionales. En definitiva, un quinto poder para una Democracia más justa.


  1. “Doing Bayesian Data Analysis” por John K. Kruschke 

5 responses so far

Scala vs. Python vs. Lua

abr 09 2013

Hace bastante tiempo que ando comentando cosas de estos tres lenguajes: Scala, Python y Lua. Hasta el momento no he hecho una comparativa entre ellos y creo que es el momento de hacerlo, siempre desde el punto de vista de un programador. Más que llegar a la conclusión de cuál es mejor o peor, quisiera dar una idea de porqué los recomiendo, a los tres, sin decantarme por sólo uno de ellos. Si buscabas razones para quedarte con uno de ellos, tampoco deberías desestimar otros similares como Ruby, Groovy, Haskel, Clojure o Erlang. De todos hay cosas qué aprender.

Python

Quizás Python sea el lenguaje más asequible para un programador que empieza o que busca un segundo lenguaje. Su aprendizaje es sencillo, mientras que su potencia y ubicuidad lo hace ideal desde los pequeños scripts que podamos necesitar en nuestro día a día, hasta escalar a servidores empresariales de tipo medio.

Puede que a muchos disguste python por su identación forzada o por su particular modelo de datos, por citar dos de las características más criticadas. Sin embargo, confía en mí si te digo que python es uno de los mayores compendios de sabiduría que puedes tener al alcance de tus manos. Cualquier cosa que creas extraña o fuera de lugar, seguramente tenga su buena explicación. El sistema colaborativo que hace evolucionar a python (conocido como PEP-Python Enhancement Proposals) consigue que todo el saber de la comunidad python termine decantándose hace un modelo de evolución del lenguaje que lo hace único, con el que mejora calmadamente con cada versión. Operaciones con números grandes, algoritmo MRO para herencia múltiple, estructuras de datos optimizadas (heapq, deque,…), ordenaciones por clave, operaciones sobre secuencias (sum, any, all,…)… son sólo algunos ejemplos de optimizaciones que el usuario usa sin ser realmente consciente de la cantidad de trabajo que le está ahorrando. En python casi siempre hay una forma de hacer las cosas correctamente, y además suele ser la mejor.

Lua

Desde mi punto de vista, considero Lua como un python minimalista. Sin objetos, sin posibilidad de construir tus propios tipos de datos, pero se apaña con un sólo tipo de estrutura table para montar un sistema de herencia y emular algunos tipos de datos. Si lenguajes como python te parece complicados, no comprendes conceptos como la herencia, la creación de tipos o para qué sirven las metaclases, la simplicidad de lua hará que entiendas mejor estos conceptos.

El reducido tamaño del intérprete de Lua lo hace apropiado para ser empotrado en otras aplicaciones. Lo tenemos en gestores de paquetes (RPM), bases de datos (mysql-lua), e IDEs (Scite), aunque quizás sea más famoso por ser el motor de script de juegos como World of Warcraft.

En cuanto a sintáxis, también goza de un minimalismo que, a veces, desearías tuviera python. Posee cierta relajación en la llamada a funciones que permite usarlo para crear DSLs (Lenguajes Específicos del Dominio), aunque quizás su mejor uso sea como lenguaje de descripción de datos en sustitución de xml, yaml o ficheros ini.

Scala

Reconozco que soy un ferviente partidario de la Programación Funcional. Python tiene algún aspecto de este paradigma, pero cada vez parece más diluido dentro del sistema de Clases Abstractas (ABC-Abstract Base Classes) que empiezan a generalizarse en python. La estrategia de python es optimizar el uso de estas clases abstractas, independientemente de las clases que deriven luego de ellas. Aunque es un buen enfoque de optimización, siempre estará limitado a tiempo de ejecución.

Scala posee un potente sistema de tipado estático de datos que posibilita la inferencia del tipo de una operación, lo que permite cierta relajación en el tipado que lo hace muy similar al tipado dinámico. Pero la posibilidad de crear nuevos tipos, ya no sólo de objetos, si no también a partir de funciones o de patrones de código, consigue interfaces más robustos y que sea el compilador quien optimize el código, antes de su ejecución.

Así que tenemos que scala es funcional, con un potente sistema de tipos y, además, 100% compatible con Java. ¿Se puede pedir algo más?

Pues sí. Incorpora el llamado modelo Actor para programación concurrente. Con los actores, en lugar de compartir un espacio común de memoria entre los distintos procesos concurrentes, se establece un sistema de mensajes que son enviados y recibidos. Este modelo se ha mostrado bastante eficaz en sistemas de alta demanda como son algunas webs como twitter o linkedin.

En cuanto a la sintáxis, scala también posee algunas normas relajadas para la creación de DSLs muy similar a lo que se ve en Groovy. Algunos lenguajes DSL se usan en frameworks de creación webs, como Play2, o para crear conjuntos de pruebas (ScalaUnit).

Conclusión

Espero que te haya convencido para que eches un vistazo a algunos de estos lenguajes, aunque los tres sean altamente recomendables. Si tuviera que resumir en pocas líneas lo dicho hasta ahora, sería así:

  • Python: navaja suiza de los lenguajes. Sirve para todo y está presente en cualquier sitio. Es un compendio de sabiduría para hacer las cosas de la mejor forma, aún sin proponértelo.

  • Lua: lenguaje minimalista. Ayuda a comprender mejor algunos conceptos de programación. Es el lenguaje que me gustaría que tuviera todo navegador en lugar de javascript.

  • Scala: funcional y con potente sistema de tipos. Su implementación del modelo actor lo hace idóneo para la creación de sistemas de alta demanda de accesos.

7 responses so far

Older posts »