Mostrando entradas con la etiqueta Python. Mostrar todas las entradas
Mostrando entradas con la etiqueta Python. Mostrar todas las entradas

miércoles, 9 de mayo de 2018

TensorFlow, machine learning I: Perceptrón multicapa

Esta entrada presupone, por parte del lector, ciertos conocimientos de redes neuronales artificiales. No obstante ofrezco una descripción teórica muy rápida.

Las redes neuronales artificiales se basan en el funcionamiento de las redes formadas por células de los sistemas nerviosos cuya principal capacidad es la de excitarse eléctricamente según una serie de estímulos.



Los estímulos son recibidos a través de las dentritas  y en función de estas entradas la neurona puede activarse y mandar estímulos a través de los axones terminales a otras neuronas o no activarse. Poniendo un ejemplo muy sencillo; supongamos que tenemos una red neuronal encargada de mandar la señal a los músculos para encestar una bola de papel en una papelera; las entradas a las neuronas podría ser al ángulo de lanzamiento y la fuerza, las redes se activan o no según una función de activación determinada que tiene como entrada el valor de las entradas multiplicadas por unos pesos -inicialmente aleatorios-. Este proceso de activación pasará por diferentes capas neuronales, cada una con su función de activación y sus pesos. Finalmente la capa de salida da un valor que se interpreta para colocar el brazo y lanzar. A continuación se observa el resultado y se corrige los pesos usando como criterio la minimización del error -por ejemplo según mínimos cuadrados-. El proceso de lanzamiento-evaluación del error-modificación de pesos-lanzamiento continua hasta que se alcanza el error deseado, a continuación se fijan esos pesos "ganadores". A partir de ese momento lanzar y encestar -en esas mismas condiciones- no debería requerir más entrenamiento, usando siempre los mismos pesos. Evidentemente esto es una simplificación muy grande pero es justo la idea que subyace en las redes neuronales artificiales.

Una vez establecidas estas pequeñas ideas teóricas vamos a implementar una red neuronal  en TensorFlow. Básicamente esto consiste en realizar el siguiente esquema:
Por sencillez vamos a usar solo una capa oculta, un solo atributo de entrada y una sola salida. Ampliar a más capas, a más entradas o más salidas es inmediato. 

Básicamente lo que tenemos que hacer es:
  1. Establecer el número de entradas.
  2. Definir cuantas capas ocultas -hidden layers- que deseamos y cuantas neuronas por capa.
  3. Cuales será los pesos iniciales -W-.
  4. Añadir el bias.
  5. Cual será la función de activación de cada capa.
  6. Definir la capa de salida. 
  7. Cual será el criterio de error.
  8. Entrenar la red.
  9.  
A continuación el código que realiza todo este trabajo:

def neural_net(input_x, output_y, total_epoch):
    x = ts.placeholder("float", [None, 1])
    y = ts.placeholder("float", [None])
    batch_size = 10
    n_input = 1
    n_output = 1
    n_hidden_layer = 10
    w_layer1 = ts.Variable(ts.random_normal([n_input, n_hidden_layer]))
    bias = ts.Variable(ts.random_normal([n_hidden_layer]))
    hidden_layer = ts.nn.sigmoid(ts.add(ts.matmul(x, w_layer1), bias))
    w_layer2 = ts.Variable(ts.random_normal([n_hidden_layer, n_output]))
    bias_out = ts.Variable(ts.random_normal([n_output]))
    output_layer = ts.matmul(hidden_layer, w_layer2) + bias_out

    cost = ts.reduce_mean(ts.square(output_layer-y))
    optimizer = ts.train.AdamOptimizer(learning_rate=0.001).minimize(cost)

    with ts.Session() as sess:
        sess.run(ts.global_variables_initializer())
        for epoch in range(total_epoch):
            error = 0
            total_batch = int(len(input_x) / batch_size)
            for i in range(total_batch - 1):
                batch_x = input_x[i * batch_size:(i + 1) * batch_size]
                batch_y = output_y[i * batch_size:(i + 1) * batch_size]
                feed_dict = {x: batch_x, y: batch_y}
                _, c, p = sess.run([optimizer, cost, output_layer], feed_dict=feed_dict)
                error += c/total_batch
        print("Training finalizado")


x = [[x/100] for x in range(-20, 20)]
y = [y[0]*y[0] for y in x]
neural_net(x, y, 200)
Y ahora una explicación más detallada.

1. Establecer el número de entradas

Esto es establecer cuantos atributos tendremos. En un ejemplo muy sencillo, para hacer regresión de una función desconocida $f(x)$, solo tendremos una entrada.
Otro aspecto importante es crear un placeholter para las entradas y las salidas.  Los placeholter son variables vacías que serán alimentadas más adelante. Si no se alimentan, cuando ejecutemos el algoritmo nos dará un error. Hay que tener en cuenta que si empleáramos Variables en lugar de placeholder los valores podrían ser sobrescritos, de esta forma aseguramos que los valores de entrada y salida para entrenamiento permanecen constantes a lo largo del entrenamiento. Es cierto que también podrían usarse constantes en vez de estas estructuras, sin embargo TensorFlow penaliza este uso limitando el tamaño de las constantes -2 Gb- .Por otro lado los placeholder pueden aumentar dinámicamente el tamaño de la memoria, lo que permite más flexibilidad a la hora de nutrir un modelo con diferentes conjuntos de entrenamiento.

x = ts.placeholder("float", [None, 1])
y = ts.placeholder("float", [None])
En este caso  las entradas se han representado como una matriz de una columna, mientras que la salida está representada por un array.
 

2. Definir cuantas capas ocultas que deseamos y las neuronas por capa

Por lo general no es necesario emplear más de dos capas ocultas, evidentemente hay una razón para ello. Por una lado hay que tener en cuenta que al añadir capas ocultas incrementamos el número de pesos, si la capa anterior tiene m neuronas -o si es la capa de entrada, m entradas- y la nueva capa oculta aporta n neuronas, tendremos en total mxn nuevos pesos. El exceso de pesos puede generar un problema de sobre aprendizaje siendo capaz de aprender "de memoria"  acomodando los pesos para cumplir con unas salidas dentro del error solicitado. El problema es que ante cualquier modificación de las condiciones no es capaz de ofrecer respuestas satisfactorias. Por otro lado el aumento de neuronas implica tiempos de cálculo crecientes. Lo más adecuado es empezar con una sola capa, ir aumentando las neuronas de esa capa esperando que el error se ajuste a lo solicitado. Si los resultados no mejoran entonces habría que pensar en el aumento de capas ocultas. Esto lo definiremos como una contante:

n_hidden_layer = 10

3. Cuales será los pesos iniciales, W. 

Como puede verse cada entrada, o cada neurona, aporta a la activación de todas las que tiene por delante según un peso. Inicialmente estos pesos son aleatorios. 

w_layer = ts.Variable(ts.random_normal([n_input,n_hidden_layer])) 

Esto nos da una matriz de pesos que une cada entrada -una en este caso- con cada una de las neuronas de la primera capa -10-.

4. Establecer el bias.

Cada entrada se multiplica con el peso que le corresponde para cada neurona -tal y como se muestra en el esquema-. Es decir, si tuviésemos 3 entradas $x_{0}, x_{1}, x_{2}$ la entrada a la primera neurona seria $x_{0}*w_{0,0}+x_{1}*w_{1,0}+x_{2}*w_{2,0}$, de igual manera para el resto de las neuronas. Sin embargo resulta conveniente añadir un grado de libertad más al conjunto (un bias), de tal forma que la entrada a la primera neurona de la capa oculta quede como:

$x_{0}*w_{0,0}+x_{1}*w_{1,0}+x_{2}*w_{2,0}+bias_{0}$

En nuestro caso como solo hay una entrada tendremos:

$x_{0}*w_{0,0}+bias_{0}$

El bias fue introducido en el modelo de red Adalina -ADAptative LInear Neuron- por Widrow y Ho (1960).

En nuestro caso el bias para la primera capa oculta lo calculamos como:

bias = ts.Variable(ts.random_normal([n_hidden_layer]))

5. Cual será la función de activación de cada capa.

Lo recibido por cada neurona es procesado por esta y activa la salida en función de la llamada función de activación $f(x_{0}*w_{0,0}+bias_{0})$. Existen diferentes funciones de activación:
  • Neurona todo/nada. Representada por una función escalón con un umbral -salida digital-.
  • Neurona continua tipo sigmoide.
Hay algunas otras funciones de activación pero estas son las más empleadas. En cualquier caso es necesario que sean derivables como exige el algoritmo de retropropagación del error. El módulo nn de TensorFlow nos proporciona un buen número de funciones de activación. En nuestro caso emplearemos la función sigmoide para calcular la salida de las neuronas de la capa oculta, que no es más que la suma de las entradas pasadas por la función de activación:

hidden_layer = ts.nn.sigmoid(ts.add(ts.matmul(input_x, w_layer), bias))
Esto último requiere una pequeña explicación, aunque realmente no es más que cálculo matricial simple. En cualquier caso tenemos:
  
ts.matmul(input_x, w_layer) -> Producto matricial de la matriz de 
entradas por la matriz de los pesos, si recordamos el producto 
matricial reconoceremos que el resultado es la suma de los productos
de las entradas por los pesos correspondientes, cada fila es justo 
la entrada a cada neurona de la capa oculta.  
 
ts.addts.matmul(input_x, w_layer), bias)) ->  Le sumamos a la matriz anterior los bias. 
6. Definir la capa de salida 
De igual manera debemos establecer lo que llega a la salida desde las neuronas de la última capa oculta -en este caso coincide también con la primera oculta-. Esto se hace con el mismo razonamiento que empleamos en el paso 5. 

w_layer2 =  ts.Variable(ts.random_normal([n_hidden_layer, n_output]))
bias_out = ts.Variable(ts.random_normal([n_output]))
output_layer = ts.matmul(hidden_layer, w_layer2) + bias_out

6. Cual será el criterio de error y el algoritmo de optimización

El aprendizaje de una red neuronal es de tipo supervisado, esto significa que deberemos disponer de un conjunto de datos con sus entradas y con sus salidas correctas, de tal manera que la propia red sepa en que medida su predicción es la adecuada, corrigiendo los pesos para mejorar su predicción. Para ello debemos dar a la red un criterio para establecer lo bien que esta realizando su tarea. La función de error cumple precisamente este cometido, también llamada función coste.

cost = ts.reduce_mean(ts.square(output_layer-y)

Como puede observarse esto no es más que el error cuadrático medio. 

Una vez establecido el error a estimar hay que decidir como buscar el mínimo. Cambiar los pesos sin criterio, al zar, no es un método efectivo para minimizar una función de coste. Entre los muchos métodos que se pueden elegir, uno muy efectivo es el de descenso de gradiente. De forma muy resumida, dada una función F que queremos minimizar, probablemente $F(x_{n+1})<F(x_{n})$ para:

$x_{n+1}=x_{n}-\gamma \bigtriangledown F(x_{n})$

El método de descenso de gradiente Adaptive Moment Estimation o Adam es una implementación más efectiva del descenso de gradiente: 

optimizer = ts.train.AdamOptimizer(learning_rate=0.001).minimize(cost)

7. Entrenar la red

Una vez configurada la red debemos empezar el entrenamiento. Básicamente esa tarea se realiza en el siguiente código:

with ts.Session() as sess:
        sess.run(ts.global_variables_initializer())
        for epoch in range(total_epoch):
            error = 0
            total_batch = int(len(input_x) / batch_size)
            for i in range(total_batch - 1):
                batch_x = input_x[i * batch_size:(i + 1) * batch_size]
                batch_y = output_y[i * batch_size:(i + 1) * batch_size]
                feed_dict = {x: batch_x, y: batch_y}
                _, c, p = sess.run([optimizer, cost, output_layer], feed_dict=feed_dict)
                error += c/total_batch
        print("Training finalizado")

Como puede verse es imprescindible crear una sesión y e inicializar las variables. A continuación corremos el entrenamiento un numero suficiente de veces epoch, que buscando un símil cotidiano equivaldría al número de clases de conducir que necesitamos para entrenar a nuestras neuronas para que sepan conducir un vehículo. En la línea 5 separamos el conjunto de entrenamiento en lotes, en la línea 9 creamos un diccionario para alimentar los placeholder y en la siguiente entremos la red, los valores _, c, p corresponde al optimizador, al coste y a la salida que proporciona la red, es decir a la predicción.

Logicamente habría que controlar el error y salir del bucle si se alcanza  antes de finalizar todos los ciclos de entrenamiento. Tampoco es suficiente con entrenar la red, a continuación habría que validar con un conjunto diferente de datos y en última instancia pasarle un último test con un tercer conjunto. Estos detalles los dejo para las siguientes entradas. En cualquier caso los resultados son los siguientes para los siguientes datos:
x = [[x/100] for x in range(-300, 300)]
y = [1+math.sin(3*y[0]) for y in x]
En azul la predicción de la red. Evidentemente la función no es extraordinariamente complicada, no obstante es útil para verificar que esta bien configurada. Una recomendación es siempre pasar a nuestra red configurada un patrón bien conocido -una función matemática- con pocos ejemplos pero suficiente para comprobar que la red esta bien configurada; por ejemplo, un valor excesivo en el factor $\gamma=0.01$ genera resultados menos satisfactorios.

Incluso con idénticos valores de entrenamiento una mala configuración de la red -en este caso simplemente hemos elegido mal el tamaño de los lotes de entrenamiento-.

Una vez comprobada la bondad de la red para un problema de regresión genérico, sabiendo que no hay fallos de construcción, podemos atacar el  problema de regresión real -evidentemente será necesario ajustar ciertos parámetros, aumentar las neuronas,..- pero sabremos que nuestra red esta bien construida.
  

martes, 27 de marzo de 2018

Python 3.x, que versión elegir




Estoy buscando la mejor versión de Python para afrontar un nuevo proyecto de machine-learning. Se trata de un proyecto muy ambicioso cuyo desarrollo se dilatará en el tiempo y que servirá también como laboratorio para ensayar la implementación de nuevas técnicas de IA. Es importante que la herramienta pueda perdurar sin tener que migrar, en mucho tiempo, a nuevas versiones de Python. He realizado un pequeño análisis para valorar cual será la mejor elección y he querido compartir este pequeño esfuerzo.

Empezaré describiendo los condicionantes que se han de cumplir:
  1. Debe ser compatible con Django 2.
  2. Debe ser compatible con la mayoría de las librerías importantes de machine-learning y matemáticas.
Antes de nada justificar la elección de 3.x y no de 2.x pero esto es inmediato si tenemos en cuenta que 2.7 tiene los días contados, concretamente el soporte finalizará en el 2020 y ya no habrá más versiones de 2.x. Una vez establecido este extremo pasamos a discutir los siguientes puntos:

1. Compatibilidad Django 2.

Como podemos ver aquí Django 2 solo es compatible con 3.x, concretamente nos ofrecen la siguiente tabla de compatibilidades:

 Django  Ptyhon
2.0:         3.4, 3.5, 3.6
2.1:         3.5, 3.6, 3.7

Ni Django 2.0 ni 2.1 son del tipo LTS, será la 2.2 la próxima LTS y como podemos ver nada se nos dice sobre la compatibilidad de Python para esta versión LTS, no obstante si confiamos en que continúen con la mima serie tendremos:

Django  Ptyhon
2.2:         3.6, 3.7, 3.8

Bajo este ángulo tenemos tres opciones, la 3.5, la 3.6 y la 3.7. Es evidente que la mejor opción en este caso es la 3.6 ya que por un lado 3.5 pierde su compatibilidad con la versión LTS (2.2) y la 3.7 necesita de la versión 2.1 de Django que saldrá en Agosto del 2018. 

La mejor elección: Python 3.6

2. Compatibilidad con librerías machine-learning.

En este apartado se incluyen todas las necesarias para el desarrollo de algoritmos y algunas muy recomendadas. A continuación una lista de las imprescindibles:

  1. Numpy: Paquete de para computación científica, imprescindible para afrontar cualquier problema matemático. La versión 1.14 requiere Python >= 2.7 pero es incompatible con 3.0, 3.1, 3.2, 3.3 y compatible con 3.4-3.6. Numpy 1.14 se ha compilado con Cython 0.26.1 que no es compatible con Python 3.7.
  2. Pandas:  Paquete para análisis de datos, totalmente imprescindible. Las versión 0.22 es compatible con 2.7, 3.5 y 3.6, por el momento no soporta 3.7.
  3. ScyPy: Totalmente imprescindible. Requiere Python >= 2.7 o Python >= 3.4, no dice nada respecto a Python 3.7. 
  4. SymPy: Matemáticas simbólicas. No es imprescindible pero si muy recomendada. La última versión 1.1.1 requiere Python 2.7, 3.3, 3.4, 3.5, 3.6.
  5. Matplotlib: Representación gráfica. No es imprescindible pero si muy recomendada. Python >= 2.7 o >= 3.4, no se dice nada respecto a 3.7.
  6. TensorFlow: API de Google para deep-learning. Muy recomendable.  Requiere 2.7 o 3.x.
  7. OpenCV: Para visión artificial. Muy recomendable. Soporta las siguientes versiones de Python:  2.7, 3.4, 3.5, 3.6. 
  8. Numba: Para correr nuestro código usando también la GPU. Muy recomendable. Sigue exactamente el mismo criterio de Numpy por lo que podemos decir que es compatible con 2.7 y 3.4-3.6 pero no es compatible con Python 3.7. 
  9. Python StatsModels: Para estadśitica. Imprescindible, requiere 2.7 o 3.x.  
La mejor elección: Python 3.6, Python 3.5 

Todo parece indicar que la mejor elección es Python 3.6, no obstante repasemos algunas de las mejoras de la versión 3.6 respecto a la 3.5:
  1. Se pueden usar guiones bajos para leer mejor los números grandes 1_000_250=1000250.
  2. Se introduce en el estándar la anotación de los tipos para las variables, listas, diccionarios y clases.
  3. Se puede usar await y async en las list, set, dict comprehensions.
  4. Se puede usar await y async en una misma función. 
  5. Se incorpora el módulo secret para generar números aleatorios válidos para criptografía.   
  6. Se agrega un nuevo protocolo file system path.
Parece que la inminente versión 3.7 de Python traerá mejoras considerables respecto a la depuración del código, sin embargo introducen un cambio en el manejo de las excepciones de tal manera que estas serán incompatibles con las versiones anteriores de Python.
Conclusión: La versión de Python 3.5 es compatible con todos los paquetes de machine-learning y aunque las mejoras de 3.6 con respecto a 3.5 no son espectaculares Python 3.5 dejará de ser compatible con la versión de Django 2.2. Por otro lado la versión 3.7 parece introducir muchas mejoras pero no estará disponible la versión estable hasta el verano del 2018. Por otro lado no es soportado por muchos paquetes debido a la notable diferencia al tratar las excepciones. Por todo ello parece que la mejor elección es Python 3.6.

miércoles, 9 de abril de 2014

La funcion Gaussiana en visión artificial II


Como decíamos en la anterior entrada dedicada a la gaussiana, su empleo  puede servir de ayuda para encontrar características en una imagen sin tener que preocuparnos por la escala. Para ver como funciona vamos a recordar un nuevo operador ampliamente conocido en el mundo de la física, se trata del operador laplanciano:


$\Delta f =\nabla^2f$

O en dos dimensiones y en coordenadas cartesianas:

$\Delta f =\nabla^2f = \frac{\partial^2f}{\partial x^2}+\frac{\partial^2f}{\partial y^2}$ 

Cuando es aplicado sobre imágenes, la función f es $I(x,y)$ y designa la intensidad del pixel de coordenadas x, y. En OpenCV podemos fácilmente calcular el resultado del operador laplanciano si tenemos en cuenta que este puede calcularse como una convolución con un kernel determinado. El siguiente ejemplo muestra lo que hace el operador laplanciano, en este caso con un kernel de 5x5:


Como puede verse la segunda derivada ofrece una gran sensibilidad al ruido. Si lo que se desea es calcular los bordes de los objetos el uso del operador laplanciano  pude ser de utilidad, pero la excesiva sensibilidad al ruido lo hace de difícil uso. No obstante podemos emplear, antes del operador laplanciano, un filtrado gaussiano. Esta nueva convolución es conocida como LoG. A grandes rasgos el filtrado LoG tendrá el siguiente comportamiento al aplicarlo a una imagen:

- Cero a lo largo de los contornos.
- Positivo justo a un lado del contorno.
- Negativo al otro lado del contorno.
- Cero en el interior del contorno.

Cuando se eligen valores de sigma pequeños el filtro LoG es capaz de captar los bordes que se encuentran a menos escala, mientras que con valores grandes se capturan los de mayor tamaño. El siguiente ejemplo muestra un LoG con un sigma de 3.


La parte correspondiente a la cabeza y al borde de las alas se distinguen con facilidad, sin embargo otras zonas mas sutiles son menos nítidas - las antenas, por ejemplo -. Si reducimos el valor de sigma, digamos a 1, tenemos el siguiente resultado:



Ahora logramos que otras características se hagan visibles. Aplicando este método, usando con cuidados los valores del tamaño del kernel y el de sigma, podemos descubrir muchas de las características notables de una imagen. Ahora bien, el LoG es costoso en tiempo de computación, aunque existe una solución si empleamos un camino opcional calculando las diferencias de convoluciones gaussianas con dos diferentes sigmas, por ejemplo $\sigma$ y $k\sigma$. Esto es una aproximación al cálculo de LoG más rápida. Esto se hace para varios valores de sigma, en forma piramidal y disminuyendo la escala - o aumentándola si partimos de un valor de sigma pequeño -. Una vez realizadas las diferencias se buscan los máximos comparándolos con 8 los píxeles vecinos, así como los 9 píxeles de los niveles de escala posteriores y anteriores. De esta forma se consiguen los extremos locales, que potencialmente pueden ser keypoints. Una vez obtenidos estos máximos locales se puede refinar aún más nuestro conjunto de keypoints desechando los que estén por debajo de un cierto umbral. Esta descripción corresponde al método SIFT, descrito en 2004, D.Lowe, University of British Columbia, Came up with a new algorithm, Scale Invariant Feature Transform. El algoritmo esta disponible en OpenCV por lo que no necesitamos implementarlo nosotros mismos. Ahora describimos en pocas líneas como usar SIFT en openCV desde Python:
 
sift = cv2.SIFT()
keypoints, descriptor = self.sift.detectAndCompute(image,None) 

Para representar los keypoints sobre la imagen podemos recurrir a  drawKeypoints, que si le añadimos DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS como flag, nos dibujará los keypoints  con su orientación y un radio proporcional a la "fuerza" del mismo. El resultado es el siguiente:





En la siguiente entrada veremos como podemos recurrir a un algoritmo aún más eficiente que el SIFT.

viernes, 14 de marzo de 2014

Evaluación de algoritmos en Python



En algunas ocasiones preocuparse por la eficiencia de un algoritmo es la diferencia entre encontrar una solución y no encontrarla. Disponer de un algoritmo que nos ofrece una solución en un tiempo imposible de asumir, es no haber encontrado la solución en absoluto. En algunos casos, cuando el tiempo no es determinante, podemos ser menos sutiles y usar la "fuerza bruta", siempre y cuando el tiempo que necesita nuestro programa para ejecutarse sea razonable. Son soluciones que, aunque poco elegantes, nos permiten seguir avanzando. No obstante evaluar nuestros algoritmos es una buena practica que permite mejorar los mismos, encontrando soluciones mas óptimas y mejorando el rendimiento. En Python tenemos algunas herramientas muy útiles, describiremos algunas y como se pueden usar:


  • Para evaluar tiempos tenemos timing, su uso resulta muy sencillo:



Una forma de uso muy útil de timing es desde la línea de comandos, un ejemplo para evaluar una función que se encuentra en un módulo (mymodule) podría realizarse como $python -m timeit -s "import mymodule as m" "m.function()"

  • Para comparar funciones o encontrar cuellos de botella, cProfile. 
Vamos a intentar comparar tres formas de calcular un factorial, para ello partimos del siguiente código:

import cProfile
def main ():
    
    def factorial1(n):
        if n == 0:
            return 1
        return factorial1(n-1)*n
    def factorial2(n):
        fac = 1
        for x in xrange(1,n+1):
            fac = fac * x
        return fac
    def factorial3(n):
        fac = 1
        for x in range(1,n+1):
            fac = fac * x
        return fac
    
    factorial1(990)
    factorial2(990)
    factorial3(990)
    
if __name__=="__main__":        
        main()

cProfile.run('main()')

El resultado que ofrece mi máquina es el siguiente:



Como puede verse el cuello de botella se encuentra en la función recursiva (factorial1). Para poder evaluar con más precisión cual de las funciones es más efectiva, debemos aumentar el límite de profundidad de recursividad, ya que factorial1 estará limitado a 950. Para aumentar tal restricción añadimos lo siguiente:

import sys
sys.setrecursionlimit(10000)

Ahora podemos calcular el factorial de un número mucho más grande, digamos 5000. El resultado es:
  


Se confirma el cuello de botella en la función recursiva resultando ganadora aquella que emplea range en lugar de xrange. Ya que estamos, aprovechemos para comprobar que es más rápido en una lista; append vs insert. 

def main2():
    def append():
        list1 = []
        for x in range(1,1000):
            list1.append(x)
            
    def insert():
        list1 = []
        for x in range(1,1000):
            list1.insert(0,x)    
    
    append()
    insert()

    
if __name__=="__main2__":        
        main2()

cProfile.run('main2()')

Los resultados del combate:



Como puede verse el método append resulta ser más efectivo que insert. 

Estas herramientas, y algunas otras, son muy efectivas a la hora de valorar los distintos algoritmos. Si se quiere ser preciso estos experimentos deberían realizarse muchas veces y con distintos tamaños o repeticiones. Por ejemplo, en el experimento anterior, insert es igual de rápido que append para pocas inserciones, sin embargo cuando estas ganan en tamaño es cuando las diferencias se hacen más notables. Visualizar los resultados mediante gráficas para poder valorar mejor los resultados también es muy importante. Python ofrece herramientas eficientes para automatizar y ofrecer estos resultados o elaborar informes, no obstante este propósito excede de la finalidad de esta entrada, pero basta decir que los módulos csv y matplotlib aportan las herramientas necesarias para tal labor .
     

jueves, 13 de marzo de 2014

Python exquisite III, decoradores



Esta entrada voy a dedicarla a algunos conceptos importantes, en muchos sentidos básicos, pero que son necesarios para entender el uso de los decoradores en Python. Más del 60% del contenido lo he sacado del blog de Simeon Flanklin, por lo que recomiendo una visita al mencionado blog.

Empecemos por conceptos sencillos que resultan imprescindibles para entender el funcionamiento de los decoradores:

  • Las funciones crean un nuevo ámbito, o un nuevo espacio de nombres para las variables. Esto es; las funciones tienen su propio espacio de nombres.
var_global = 10
>>> def fun():
...     var_local = -10
...     var_global = -1
...     print var_global
...     print locals()
... 
>>> fun()
-1
{'var_local': -10, 'var_global': -1}
>>> var_global
10
>>> globals()
{'__builtins__': , 'var_global': 10, 
'__package__': None, 'fun': , '__name__': '__main__', '__doc__': None}


Como puede verse las variables globales no se modifican dentro de una función, se puede trabajar con ellas pero realmente se trabajaría con una copia local. De igual manera las variables locales solo son visibles desde la función que las crea.
  • Python permite funciones dentro de funciones, es decir, se permiten las funciones anidadas. 

x_global = -10
def fun_out():
    x = 10 + x_global
    print locals()
    def fun_inn():
        print locals()
        x = x_global + 1 
        print locals()
    fun_inn()
    print locals()
fun_out() 

Al ejecutar lo anterior tenemos:



x_global es una variable global por lo que no puede modificarse dentro de las funciones, por otro lado x es una variable local para la función fun_out , al igual que la función interna fun_inn. Un detalle importante es que fun_inn no puede ver la variable local x de fun_out, es decir, las variables locales de fun_out no pasan a ser globales para las funciones internas. 

  • En Python las funciones admiten funciones como argumentos, también pueden devolver funciones. 
 
def operation(func,x,y):
    return func(x,y)
def mult(x,y):
    return x*y
def div(x,y):
    return x/y if y != 0 else "Division por cero"

print operation(div,12,0)

La salida es:



  • Closures: En principio el siguiente código no debería funcionar:
def outer(x):
    def inner():
        print x
    return inner

print0 = outer(0)
print1 = outer(1)
print print0.func_closure
print0()
print1()
>> 0
>> 1 

Y no debería funcionar ya que x es una variable local de la función outer y como ouder devuelve la función inner, no se ejecuta inner hasta que outer ha finalizado. x solo existe mientras dura outer, por tanto cuando inner hace uso de x, esta ya no debería existir al haber desaparecido junto con outer. Sin embargo Python cuenta con las Closures  de una función, que es la capacidad que tiene inner para recordar estas variables locales aunque su ámbito este destruido.

  •  Decoradores: Fijémonos en el siguiente código:

class Vector(object):
    def __init__(self,vx,vy,vz):
        self.x = vx
        self.y = vy        
        self.z = vz
    def __repr__(self):
        return " Coordinates " + str(self.__dict__)

def prod(v,w):
    return Vector(v.y*w.z - v.z*w.y,v.z*w.x - v.x*w.z, v.x*w.y - v.y*w.x)

v = Vector(2,0,1)
w = Vector(1,-1,3)
v = prod(v,w)  


La función producto realiza el producto vectorial, pero nos dará un error si intentamos realizar el producto entre un escalar y un vector. Si quisiéramos incorporar el producto entre un escalar y un vector podríamos añadir una nueva función, complicar algo más el código de la función producto o realizar una función DECORADORA  que modifique prod añadiéndole nueva funcionalidad, es decir, "decorando" nuestra función producto. Usando una función decoradora tendríamos:

class Vector(object):
    def __init__(self,vx,vy,vz):
        self.x = vx
        self.y = vy        
        self.z = vz
    def __repr__(self):
        return " Coordinates " + str(self.__dict__)

def prod(v,w):
    return Vector(v.y*w.z - v.z*w.y,v.z*w.x - v.x*w.z, v.x*w.y - v.y*w.x)

def cover(fun):
    def check(v,w):
        if type(v) == type(1) or type(v) == type(1.0):
            ret = Vector(v*w.x,v*w.y,v*w.z)
        else:
            ret = fun(v,w)
        return ret
    return check

prod = cover(prod)
v = Vector(2,0,1)
w = Vector(1,-1,3)
q = prod(v,w)
r = prod(2,w)
print q
print r 

La salida funciona como esperabamos:



La función cover es precisamente un decorador y modifica o decora a cualquier función (que tenga los mismos argumentos que check).

EL símbolo "@" puede ser usado para decorar una función desde su implementación. El anterior ejemplo podría sustituirse por el siguiente código:

class Vector(object):
    def __init__(self,vx,vy,vz):
        self.x = vx
        self.y = vy        
        self.z = vz
    def __repr__(self):
        return " Coordinates " + str(self.__dict__)


def cover(fun):
    def check(v,w):
        if type(v) == type(1) or type(v) == type(1.0):
            ret = Vector(v*w.x,v*w.y,v*w.z)
        else:
            ret = fun(v,w)
        return ret
    return check
    
@cover
def prod(v,w):
    return Vector(v.y*w.z - v.z*w.y,v.z*w.x - v.x*w.z, v.x*w.y - v.y*w.x)    

v = Vector(2,0,1)
w = Vector(1,-1,3)
q = prod(v,w)
r = prod(2,w)

De esta forma prod  se decora mediante cover. 

  • *args y **kwargs: Un problema evidente de los decoradores es que solo decoran funciones con un número de argumentos determinado. Por ejemplo, cover solo puede decorar funciones con dos argumentos, por lo que es difícil generalizarlo a otros casos. Esto tiene una solución, pero antes vamos a explicar *args y **kwargs. Con *args accedemos a una lista arbitraría de argumentos, el siguiente ejemplo es clarificador:
>>> def add_one(*args):
>>>     return list(x+1 for x in args)
>>> print add_one(1,2,3,4)
>>> (2,3,4,5)


Es decir, con *args empaquetamos los argumentos en una tupla y ya no hay que preocuparse de la cantidad de argumentos. **kwargs funciona de forma parecida pero trabajando con diccionarios, un ejemplo lo deja claro:

>>> def foo(**kwargs):
>>>    print kwargs
>>> print foo(a=1,b=2)
>>> {'y': 2, 'x': 1}
 
 También pueden usarse clases como decoradores, el siguiente ejemplo ilustra como podemos añadir un pequeño decorador que ofrece logging para cada función que se quiera decorar:

class LogFunc(object):
    def __init__(self,func):
        self.func = func
    def __call__(self,*args):
        print "[LOGGING] Entrando en la funcion: ", self.func.__name__
        print "[LOGGING] Con argumentos ", args        
        ret = self.func(*args)
        print "[LOGGING] Saliendo de ", self.func.__name__, " y ofreciendo el resultado" 
        return ret
        

@LogFunc 
def function1(x,y):
    return x+y
@LogFunc
def function2(x,y,z):
    return x+y++z
    
print function1(1,2)
print function2(1,2,5)

La salida es la siguiente:



 Para ver más ejemplos útiles de decoradores se puede consultar aquí.

sábado, 8 de marzo de 2014

Python y el problema del jugador



El problema del jugador, el problema del paseo del borracho o del camino aleatorio, es un clásico de la probabilidad que tiene enorme interés en distintos campos de matemática aplicada, como en la segmentación de imágenes o en las finanzas. El problema, en su formulación más sencilla, podría describirse de la siguiente manera:

Sea un jugador que partiendo de una cantidad inicial de dinero, desea alcanzar un cierto objetivo apostando en un juego. La mecánica del juego es tal que si el jugador gana (con probabilidad p) se embolsa una cantidad igual a la apostada, perdiendo el dinero de la apuesta (con probabilidad q) en caso contrario. El juego solo finaliza si el jugador alcanza su objetivo o si termina en la banca rota. El problema consiste en establecer la probabilidad de terminar en bancarrota antes de alcanzar el objetivo, en función del objetivo y de la cantidad de dinero con la que se parte, así como de las probabilidades p y q. 


Vamos a resolver el problema usando probabilidad, de forma teórica, para luego solucionarlo de forma empírica empleando Python. Ambas deberían converger cuando el número de ensayos es lo suficientemente grande (Ley de los grandes números). 
 
Supongamos que la cantidad apostada es tratada como unidad, no hay problema en esto ya que todas las apuestas deben ser iguales a lo largo del juego. Sea $u_{j}$ la probabilidad de que termine en la bancarrota y j la cantidad con la que se parte. En ese caso es claro que:

$u_{j}=p\cdot u_{j+1}+q\cdot u_{j-1}$

Con las condiciones de frontera:

$u_{0}=1$ y $u_{c}=0$ , siendo c el objetivo

Tras algunas operaciones algebraicas, obtenemos la probabilidad buscada:

 $u_{j}=\frac{r^{j}-r^{c}}{1-r}$  siendo r=q/p

Para los detalles matemáticos se puede consultar cualquier libro de probabilidad que trate el tema de los caminos aleatorios, como por ejemplo Elementary Probability Theory, K. L. Chung. Springer (2003).

Evidentemente este resultado solo es válido si r es distinto de 1 ya que en ese caso la probabilidad nos da una indeterminación. Para el caso en el que r=1, se llega a la siguiente conclusión:

$u_{j}=\frac{c-j}{c}$

Una vez establecido el marco teórico, vamos a realizar un algoritmo que realice el papel de jugador. Empecemos con el caso más sencillo, aquel en el que las probabilidades son 1/2 para el jugador y la banca, esto es p=q=1/2, por lo que r=1. 

import random as rnd
def gambler(money0,objective, bet = 1): 
    cont = 1
    lines =[]
    num =  money0 
    while (num > 0 and num < objective):
        num = num + rnd.choice([-bet,bet])
        lines.append([cont,num])
        cont= cont + 1
    return lines  

Como puede verse el algoritmo es muy sencillo, se parte de una cantidad inicial de dinero (money0), de una apuesta (bet) que tomaremos como 1, y un objetivo. En la función un número pseudoaleatorio [-1,1] con idéntica probabilidad  hace las veces de resultado. Los distintos juegos se van sumando (o restando) a la cantidad inicial, el bucle continua hasta que se alcanza el objetivo o hasta que el jugador cae en bancarrota. El resultado de los distintos juegos se mete en una tupla (línea 7).

A continuación dibujamos el resultado de las partidas. Para el caso que nos ocupa he elegido pygame para representar los resultados. La función que dibuja los distintos lances de la partida es como sigue:

import pygame as pyg
def draw(points,color,screen,init_point):
    point0 = init_point
    point_end = [0,150]
    for point in points:
        point_end = [point_end[0] + 10, point[1] + 150]
        print point_end
        pyg.draw.line(screen,color,point0,point_end,1)
        point0 = point_end

En la línea 8 encontramos la función que dibuja la gráfica con los resultados, su uso es muy intuitivo y no requiere demasiadas explicaciones. Antes de poder dibujar con Pygame es necesario inicializar el área donde se va a dibujar, esto se realiza de la siguiente forma:

screen = pyg.display.set_mode((1000, 250 + OBJECTIVE))
pyg.init()
pyg.display.set_caption('Game')
pyg.display.flip()    

La línea 4 es necesaria para refrescar el contenido de la ventana del dibujo y debe ser invocada cada vez que pintemos algo. El resultado de un solo juego, queda gráficamente como:


Se han añadido los resultados de calcular la frecuencia relativa, así como los casos ganados y perdidos. También figura la probabilidad calculada según la formula ofrecida más arriba para el caso r=1. El resultado que arrojan para este caso es:



Los datos ofrecidos no arrojan demasiada información. Las frecuencias relativas no se parecen en nada a los cálculos teóricos. Al aumentar el número de jugadas, a 100, por ejemplo, los resultados van cambiando:

 
Los resultados numéricos son:



Como puede comprobarse los valores teóricos empiezan a parecerse a los resultados empíricos. Si aumentamos el número de juegos a 100000 tenemos que:



Aquí las frecuencias relativas son casi idénticas a las obtenidas mediante el pequeño juego que hemos elaborado. Si quisiéramos trabajar con probabilidades distintas de r=1 seria necesario modificar:


 
Ya que esta función de random selecciona entre -bet y bet con igual probabilidad. Una solución sencilla es emplear sample(population,k), donde population es una población y k la longitud de la lista extraída de population. En el siguiente ejemplo (+1) tiene una probabilidad de  0.333 mientras que (-1) de 0.666:

num_pos = [x/x for x in xrange(1,4)]
num_neg = [-x/x for x in xrange(1,7)]
space = num_pos + num_neg    
rnad_num = rnd.sample(space,1)[0] 

Los dibujos que representan los distintos caminos que sigue la jugada, hasta alcanzar la bancarrota o el objetivo, forman una curiosa forma geométrica.  Es un caso unidimensional de un paseo aleatorio también podría decirse que es un paseo aleatorio por una recta. La figura cumple con los requisitos de dimensión fraccionaria, lo que los convierte en una figura fractal. Son una versión sencilla de la curva de Korch, o una curva aleatoria de Peano, con un generador mucho más sencillo. Si se desea profundizar en este tipo de fractales recomiendo la lectura de los capítulos 7,8,9 y 10 del magnifico libro La Geometría Fractal de la Naturaleza, Benoît Mandelbrot, TusQuets (2003). 


jueves, 27 de febrero de 2014

Python básico III, funciones anónimas



Python permite un uso muy peculiar de unas funciones denominadas anónimas y que no todos los lenguajes incorporan. La forma habitual de escribir una función es:

>>> def pow(b,n):
...     return b**n
... 

Pero también hay otra forma de escribir la misma función mediante las funciones anónimas:

>>> f = lambda b,n: b**n
>>> f(2,3)
8

Como puede comprobarse la función anónima viene definida con el término lambda y carece de return. Estas funciones también admiten valores por defecto, la siguiente función anónima es perfectamente válida.

>>> fun = lambda x, y = 10: y**x
>>> fun(1)
10
>>> fun(2,2)
4
>>> 
 
Otro ejemplo de como usar estas funciones es el siguiente:

>>> def pow(b): return lambda x: b**x
... 
>>> f = pow(2)
>>> f(3)
8

Mediante esta forma de encapsular las funciones podemos crear múltiples variantes de la misma función que pueden ser empleadas de forma independiente. En el ejemplo anterior también es posible llamar a la función directamente, sin tener que asignarla antes a una variable:

>>> pow(2)(3)
8
 
La potencialidad de las funciones anónimas aumenta si se emplean en combinación con la función filter. Mediante filter, podemos pasar a una función anónima todos los valores de una variable iterable,  el resultado será solo los valores que al pasar por la función devuelvan True. Veamos un ejemplo:

>>> n = range(1,20000)
>>> len(filter(lambda x: ((x+1)**2 - x**2)%2 !=0, n)) == len(n)
True

En la línea 1 creamos una lista de números entre el 1 y el 20000, en la segunda línea los pasamos por una función anónima que nos devuelve True cuando la diferencia entre el cuadrado de su consecutivo y el cuadrado del número es impar. También comparamos la longitud retornada con la longitud original de n. El resultado es True para cualquier conjunto de enteros si se considera que la diferencia de dos consecutivos enteros siempre es un impar. 

Existen otras funciones que podemos emplear. Por ejemplo, con la función map conseguimos pasar a la función lambda los valores de una variable iterable que nos devolverá los valores obtenidos:

>>> par = range(2,20,2)
>>> par
[2, 4, 6, 8, 10, 12, 14, 16, 18]
>>> map(lambda x: x**2, par)
[4, 16, 36, 64, 100, 144, 196, 256, 324]

Otra función muy útil es reduce, con ella reducimos el contenido de una variable iterable a un solo valor. La reducción se realiza según se indica en la función lambda que debe ser de dos variables. El siguiente ejemplo suma los 10 primeros números enteros.

>>> reduce(lambda x, y : x+y, [1,2,3,4,5,6,7,8,9,10])
55


miércoles, 26 de febrero de 2014

Python basico II, slices



Hay una forma cómoda de extraer elementos de una lista, se trata de las extended slices, similares las operaciones para acceder a los elementos de una matriz en Matlab o en numpy. La notación es como sigue, siendo "a" una lista, una tupla o un string:

  • a[start:end] -> Lista los elementos a partir de  la posición start, sin incluir, hasta end, es decir, matemáticamente (start,end].
  • a[start:] -> A partir de start, sin incluir, hasta el final de la lista, es decir (start, final_lista] en matemáticas.
  • a[:end] -> Desde el comienzo de la lista hasta el final.
  • a[:] -> Todo el array.
Los valores de star y end pueden ser negativos, en ese caso el comportamiento varia ligeramente, los siguientes ejemplos son clarificadores:

>>> a = [1,2,3,4,5]
>>> a[-1]
5
>>> a[-2]
4

Los resultados explican su comportamiento, que resulta muy intuitivo.

>>> a[-2:]
[4, 5]
>>> a[-3:]
[3, 4, 5]
Si se quieren sacar todos los valores menos los n últimos, se emplea a[:-n], es decir:


>>> a[:-1]
[1, 2, 3, 4]
>>> a[:-4]
[1]

Desde la versión 1.4 de Python se puede añadir un tercer termino, el paso o step. Con esta nomenclatura tenemos a[start: end: step]. Algunos ejemplos:


>>> b = range(1,10)
>>> b
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> b[0:9:2]
[1, 3, 5, 7, 9]
>>> b[::2]
[1, 3, 5, 7, 9]
Obsérvese la equivalencia de las dos últimas expresiones.  También se pueden usar expresiones negativas en el step, en ese caso el proceso se realiza desde el fina hasta el principio:

>>> b[::-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1]
>>> b[::-2]
[9, 7, 5, 3, 1]

También podemos hacer asignación.

>>> c = [1,2]
>>> c[1:3] = [-1,-2,-3]
>>> c
[1, -1, -2, -3]

Las posibilidades son numerosas y muy flexibles:

>>> d = range(0,5)
>>> d
[0, 1, 2, 3, 4]
>>> d[2::2]
[2, 4]
>>> d[2::2] = [0,0]
>>> d
[0, 1, 0, 3, 0]

También permite eliminar elementos de forma selectiva:

>>> z = range(1,10)
>>> z
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> del z[::2]
>>> z
[2, 4, 6, 8]
>>> 


En la siguiente entrada de Python básico continuo con el tema de las funciones anónimas.

Python básico I, diccionarios




Comienzo una serie de entradas para complementar algunos de los aspectos tratados en Python exquisite. Se trata, por lo general, de conceptos básicos pero fundamentales que quisiera tener por escrito. Esta primera entrada versará sobre los diccionarios. 

Un diccionario es una serie de datos indexados por una clave, la cual puede ser numérica o no. La forma habitual de crear un diccionario es mediante la combinación de la clave y el valor o dato en la forma key : value. Hay varias formas equivalentes de construir un diccionario:


>>> a =dict(one=1, two=2)
>>> b ={'one' : 1, 'two' : 2}
>>> c ={'two' : 2, 'one' : 1}
>>> a == b == c                                                                                                  
True             

La clave no puede ser modificable y debe ser única. Por ejemplo, no podemos emplear una lista como clave, ya que la lista puede ser modificada de alguna manera, por ejemplo mediante append(). En el ejemplo anterior también se observa que los diccionarios no son conjuntos ordenados, el orden es irrelevante.

Hay algunas formas más complejas de crear diccionarios:

>>> e = {x: x**2 for x in (1,2,3)}
>>> e
{1: 1, 2: 4, 3: 9}

Para acceder a todo el diccionario se puede emplear items(), para una lista de las claves keys() y values() para una lista de valores:


>>> 
e = {n: n**2 for n in range(1,5)}
>>> e.items()
[(1, 1), (2, 4), (3, 9), (4, 16)]
>>> e.keys()
[1, 2, 3, 4]
>>> e.values()
[1, 4, 9, 16]

A continuación un ejemplo más completo para acceder y añadir datos a un diccionario. Se trata de un catálogo de una biblioteca con título y número ejemplares disponibles (línea 1), el título hace el papel de clave. Compramos un libro nuevo, verificamos si existe en el catálogo (línea 2), si existe se suma un ejemplar al título ya presente (línea 3), en caso contrario se añade al catálogo.

>>> books = {'Crimen y castigo': 3, 'Calculus': 5}
>>> if 'Cronicas marcianas' in books:
...     books['Cronicas marcianas'] = books['Cronicas marcianas'] + 1
... else:
...     books['Cronicas marcianas'] = 1
... 
>>> books 
{'Calculus': 5, 'Crimen y castigo': 3, 'Cronicas marcianas': 1}

Ahora fusionamos dos bibliotecas (books2) incorporando todos sus títulos a nuestra biblioteca (books):

>>> books2 = {'La conjura de los necios': 2}
>>> books.update(books2)
>>> books
{'Calculus': 5, 'La conjura de los necios': 2, 'Crimen y castigo': 3
, 'Cronicas marcinas': 1}

Los diccionarios no son colecciones ordenadas y aunque muchas veces aparecen en orden en otras muchas encontraremos que la disposición se presenta al azar. Cuando nos resulta imprescindible un orden podemos emplear OrderedDict. 

>>> from collections import OrderedDict
>>> ordered = OrderedDict((num,str(num)) for num in range(1,7))
>>> ordered.items()
[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5'), (6, '6')] 
Cerramos esta entrada sobre los diccionarios explicando como eliminar elementos, limpiar un diccionario o eliminarlo por completo

>>> del ordered[1]
>>> ordered.items()
[(2, '2'), (3, '3'), (4, '4'), (5, '5'), (6, '6')]
>>> ordered.clear()
>>> ordered.items()
[]
>>> del ordered
>>> ordered.items()
Traceback (most recent call last):
  File " in="" line="" module="" stdin=">">NameError: 
name 'ordered' is not defined
 
La siguiente entrada sobre Python básico versará sobre los slices.

lunes, 24 de febrero de 2014

Python exquisite II, clases y la función super




Esta y las siguientes entradas tratarán sobre algunos aspectos relacionados con las clases. Primeramente, tenemos super, que es una función build-in que sirve para acceder a atributos que pertenecen a una clase superior. Un ejemplo típico:

class Madre(object):
    def apellido(self):
        print " Gonzalez "

class Hijo(Madre):
    def nombre(self):
        print " Manuel "        
        super(Hijo,self).apellido()

hijoA = Hijo()
hijoA.nombre()

Como puede verse, su uso resulta muy sencillo y evidente. No obstante en caso de herencia múltiple el uso de super puede resultar mucho menos evidente. Compliquemos el ejemplo anterior:

 
class Madre(object):
    def apellido(self):
        print " Gonzalez "
class Padre(object):
    def apellido(self):
        print " Sanz "
class Hijo(Madre, Padre):
    def nombre(self):
        print " Manuel "        
        super(Hijo,self).apellido()
class Hija(Padre,Madre):
    def nombre(self):
        print " Sofia "        
        super(Hija,self).apellido()
hijoA = Hijo()
hijoA.nombre()      
hijoB = Hija()
hijoB.nombre()  
print Hijo.__mro__
print Hija.__mro__ 

En este caso el asunto se complica, tanto las clase Padre, como la clase Madre tienen un método nombre y cuando se llama desde Hijo o desde Hija, el resultado es diferente. Para conocer la prioridad en cuanto a la herencia se puede emplear MiClase.__mro__, esto nos ofrece la secuencia de herencia y desvela las prioridades. Para la salida de Hijo.__mro__ tenemos:




La salida es bastante esclarecedora, la primera herencia es de madre, seguida de padre, por lo que como se podría esperar super llama al método de la clase Madre. En este caso la cosa resultaba bastante evidente pero en el caso de herencias más complejas __mro__ aporta información imprescindible.

El uso de super pude resultar muy peligroso si se emplea mezclado con llamadas __init__ a las clases superiores. Veamos el siguiente ejemplo:


class ClaseA(object):
    def __init__(self):
        print "Clase A"
        super(ClaseA,self).__init__()
class ClaseB(object):
    def __init__(self):
        print "Clase B"
        super(ClaseB,self).__init__()
class ClaseC(ClaseA,ClaseB):
    def __init__(self):
        print "Clase C"
        ClaseA.__init__(self)
        ClaseB.__init__(self)

clase = ClaseC() 



El uso de __init__ desde las clases que heredan para llamar a los constructores de las clases superiores es, por decirlo de alguna manera, el método clásico, mientras que super es una forma más actual de realizar el trabajo. La mezcla de lo nuevo y lo clásico no parece resultar muy correcta. Desde luego no es el resultado esperado, la segunda llamada a B no debía haberse producido. Esto es el resultado de mezclar la clásica forma de llamar a los constructores __init__ , con la más actual de usar super. Como regla general, si se emplea super en alguna clase, debe mantenerse esta forma en todas las clases que puedan heredar de ella. En este caso el problema se resuelve sustituyendo las llamadas a los constructores __init__ de las clases ClaseA y ClaseB que se realiza desde ClaseC por un super(ClaseC,self).__init__().

Otro conflicto importante ocurre cuando queremos pasar argumentos desde la clases hijas hacia las clases superiores. El siguiente ejemplo da un error.

class ClaseA(object):
    def __init__(self):
        print "Clase A"
        super(ClaseA,self).__init__()
class ClaseB(object):
    def __init__(self):
        print "Clase B"
        super(ClaseB,self).__init__()
class ClaseC(ClaseA):
    def __init__(self,arg):
        print "Clase C", "arg = ", arg
        super(ClaseC,self).__init__()
class ClaseD(ClaseB):
    def __init__(self,arg):
        print "Clase D", "arg = ", arg
        super(ClaseD,self).__init__()
class ClaseE(ClaseC,ClaseD):
    def __init__(self,arg):
        print "Clase E", "arg = ", arg
        super(ClaseE,self).__init__(arg)
        
clase0 = ClaseE(10)


Lo que ocurre, es que los argumentos se pierden y ya no están presentes en ClaseA por lo que tampoco los va a recibir ClaseD, que si los necesita en el constructor. La regla general para evitar esto; siempre hay que pasar todos los argumentos recibidos al super, y si las clases incluyen varios argumentos, añadir  *args y **kwargs. La solución quedaría como:

 
class ClaseA(object):
    def __init__(self, *args, **kwargs):
        print "Clase A"
        super(ClaseA,self).__init__(*args, **kwargs)
class ClaseB(object):
    def __init__(self,*args, **kwargs):
        print "Clase B"
        super(ClaseB,self).__init__(*args, **kwargs)
class ClaseC(ClaseA):
    def __init__(self,arg,*args, **kwargs):
        print "Clase C", "arg = ", arg
        super(ClaseC,self).__init__(arg, *args, **kwargs)
class ClaseD(ClaseB):
    def __init__(self,arg, *args, **kwargs):
        print "Clase D", "arg = ", arg
        super(ClaseD,self).__init__(*args, **kwargs)
class ClaseE(ClaseC,ClaseD):
    def __init__(self,arg,*args, **kwargs):
        print "Clase E", "arg = ", arg
        super(ClaseE,self).__init__(arg,*args, **kwargs)
        
clase0 = ClaseE(10)

El resultado es el esperado:



Algunos ejemplos en esta línea los encontramos aquí. A modo de resumen:
  • Si se emplea super hay que documentarlo correctamente.
  • Tener mucho cuidado con los argumentos con los que se llama a super, en cualquier caso usar todos los argumentos que la clase reciba.
  • Emplear *args, **kwargs, es decir,  hacer super(MiClase,self).metodo(todos_los_argumentos_pasados, *args, **kwargs)
  • No mezclar super con llamadas __init__ a las clases padres. 
En líneas generales es preferible usar super únicamente cuando se tiene una herencia múltiple en esquema de diamante.  

viernes, 21 de febrero de 2014

Python exquisite I, listas, generadores y conjuntos



Python es un lenguaje claro, sencillo de aprender y que te permite dedicar casi todo el esfuerzo a los algoritmos sin preocuparte en exceso de los problemas formales del lenguaje. No obstante, llega un momento en el que uno empieza a desear escribir código más eficiente y elegante. Comienzo, con esta entrada, a tratar algunos de los detalles menos evidentes del lenguaje, con la finalidad de profundizar aún más en Python. Si el tiempo me lo permite, espero que sea el inicio de una larga serie de entradas del mismo tipo.


Empecemos con las listas. Se pueden meter cosas de cierta complejidad en una lista, por ejemplo, una lista de pares:


pares = [num for num in range(1,100000) if num%2 == 0] 

Por su puesto el contenido podría complicarse mucho más, pero no es este el tema de discusión de esta entrada. Pensemos en términos de memoria. Hay que tener en cuenta que la lista anterior contiene 49999 números: 

>>> getsizeof(pares)/1024
396
 
Es decir, ocupa 396 KB. Si queremos simplemente sumar toda la lista de números, la cosa seria tan sencilla como hacer:

>>> sum(pares)
2499950000
 
Pero hay otra forma de realizar este cálculo, sin tener que ocupar tanta memoria. Para ello usamos un generador, que en apariencia hace lo mismo que la lista.

 
>>> paresGen = (num for num in range(1,100000) if num%2 == 0) 
>>> getsizeof(paresGen)/1024
80
>>> sum(paresGen)
2499950000

El tamaño es de tan solo 80 Bytes, frente a los 396 KB de la lista y el resultado de la suma es el esperado, como no podía ser de otra forma.

En líneas generales un generador nos proporciona una función que se comporta como un iterador, por lo que puede ser usado en un bucle. Por otro lado tenemos xrange y range, que funcionan como un generador el primero y una lista el segundo.

list = range(1,10)
>>> getsizeof(list)
144
>>> genX = xrange(1,10)
>>> getsizeof(genX)
40

Se puede pasar el generador a una lista mediante list, de la siguiente forma:

newList = list(xrange(1,10000))


Una advertencia sobre el uso de generadores; un generador no contiene realmente todos los elementos, por lo que si queremos usar los elementos hay que iterar sobre ellos. Las funciones Build-In, tal como min(), max(), sum(),... iteran sobre el generador al emplearlas, pero una vez usado el iterador no se reinicia, por lo que si usamos dos veces la función nos dará un error al trabajar sobre un objeto vacío.

genB = (num for num in range(1,1000) if num%2 == 0)
>>> min(genB)
2
>>> min(genB)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: min() arg is an empty sequence

En líneas generales xrange no presenta grades ventajas sobre la lista tradicional, excepto que tengamos un rango realmente grande y/o una máquina con escasa memoria. Para algunos detalles más ver XRange Type.

Los conjuntos (set) son muy similares a las listas pero no toman duplicados ni ordenan los elementos. Otra particularidad es que los conjuntos no tienen por que mostrarse en el mismo orden en el que se introdujeron. Su uso es útil, por ejemplo, cuando se desea saber si un elemento esta en el conjunto. Vemos un ejemplo:

  
>>> def list_letters(letters):
...     return set(letters.lower())
... 
letters = list_letters('abcdeerfsdcad')
>>> letters
set(['a', 'c', 'b', 'e', 'd', 'f', 's', 'r'])
>>> 'a' in letters
True
>>> 'z' in letters
False

Para añadir elementos podemos emplear add, no hay posibilidad de usar append  por razones obvias ya que esta última función incorpora los elementos al final del conjunto, por lo que el orden sería importante, orden que no permite set. De igual manera se puede emplear remove para quitar elementos. Hay que tener precaución con remove ya que si el elemento a eliminar no se encuentra en el conjunto tendremos un error KeyError que deberíamos manejar. Si no deseamos estar atento del manejo de este error, podemos emplear discard que realiza la misma función pero no da error alguno. Hay otras funciones interesantes que no vamos a describir aquí. Resulta más interesante describir algunas operaciones del álgebra de conjuntos. A continuación la unión, intersección y diferencia (A/B):


>>> {1,2,3} | {1,5,6}
set([1, 2, 3, 5, 6])
>>> {1,2,3} & {1,5,6}
set([1])
>>> U = {1,2,3} & {1,5,6}
>>> I = {1,2,3} & {1,5,6}
>>> {1,2,3} - {1,3,7}
set([2])
 
También la diferencia simétrica (symmetric_difference), operación que consiste en la unión de A y B menos la intersección de ambos.

>>> {2,4,6,8}.symmetric_difference({2,6,10})
set([8, 10, 4])

Se puede preguntar si un conjunto A es subconjunto de un tercero B, o si por el contrario es un superconjunto del anterior.

>>> {1,2,3,4}.issubset({1,2,3,4,5,6})
True
>>> {0,1,3,7,8}.issuperset({0,7,8})
True 

Hasta aquí un primer resumen de nociones no necesariamente básicas de Python. La siguiente entrada de Python exquisite tratará sobre clases.