Manejo de atributos mediante propiedades.

En Python, cuando creamos una clase, definimos una serie de atributos (que indicarán su estado) y de métodos (que marcarán su comportamiento). El acceso a ambos se realiza de manera sencilla mediante el operador punto. Un ejemplo de ello (como nota decir que todo el código mostrado en esta entrada está escrito en Python 3 sobre el IDE PyScripter)  es el siguiente:

class MiClase:
    def __init__(self, midato):
        self.dato = midato
    def cuadrado(self):
        return self.dato ** 2
a = MiClase(12.72)
print("El cuadrado es:", a.cuadrado())
a.dato = 7.72
print("El cuadrado es:", a.cuadrado())

La salida sería:

>>> 
El cuadrado es: 161.79840000000002
El cuadrado es: 59.5984
>>>

Para ejemplos sencillos podría ser suficiente actuar de esta manera, pero en otros momentos necesitaremos más versatilidad. Imaginemos que queremos validar los atributos con una determinada condición. En nuestro caso podría ser que los valores de dato estuviesen entre 0 y 10. A los valores menores que 0 se les asignaría el 0 y a los mayores de 10, el 10. Todo ello se lograría fácilmente creando un método que manejase esa condición y ejecutándolo también en __init__():

class MiClase:
   def __init__(self, midato):
       self.set_dato(midato)
   def set_dato(self, midato):
       if isinstance(midato, (int, float)):
           if midato < 0:
               self.dato = 0
           elif midato > 10:
               self.dato = 10
           else:
               self.dato = midato
       else:
           raise ValueError("Dato no válido")
   def cuadrado(self):
       return self.dato ** 2
a = MiClase(12.72)
print("El cuadrado es:", a.cuadrado())
a.set_dato(7.72)
print("El cuadrado es:", a.cuadrado())

Con salida:

>>> 
El cuadrado es: 100
El cuadrado es: 59.5984
>>>

En el método set_dato() hacemos la comprobación que el dato es entero o real. De lo contrario lanzamos una excepción. Con ello nos aseguraríamos que el dato cumple las condiciones que queremos.

De la misma forma que hemos creado un método para manejar la escritura del atributo dato podríamos hacerlo para su lectura. Son los denominados, de forma genérica, métodos getter y setter, que nos permiten, respectivamente, obtener o modificar los atributos. El concepto de encapsulación de datos incluye, además del uso de estos métodos, el que los atributos sean privados (no accesibles desde fuera de la propia clase), algo que en Python se logra añadiendo un doble guión bajo al nombre del atributo. Tendríamos lo siguiente:

class MiClase:
    def __init__(self, midato):
        self.set_dato(midato)
    def get_dato(self):
        return self.__dato
    def set_dato(self, midato):
        if isinstance(midato, (int, float)):
            if midato < 0:
                self.__dato = 0
            elif midato > 10:
                self.__dato = 10
            else:
                self.__dato = midato
        else:
            raise ValueError("Dato no válido")
        return None
    def cuadrado(self):
        return self.__dato ** 2
a = MiClase(12.72)
print("El cuadrado es:", a.cuadrado())
a.set_dato(7.72)
print("El cuadrado de", a.get_dato(), "es", a.cuadrado())

La salida es:

>>> 
El cuadrado es: 100
El cuadrado de 7.72 es 59.5984
>>>

En Python es habitual que los atributos que en un principio son simples datos estáticos con el tiempo se conviertan en expresiones a calcular dinámicamente o, como ya hemos visto, haya que validarlos de alguna manera. Si actuamos con métodos getter y setter desde el principio podremos hacer esa nueva implementación sin cambiar la forma en la que accedemos a los datos, pero si lo hemos hecho simplemente mediante el operador punto, estaríamos en una situación delicada. Es para superar este escollo, o conseguir una forma mas pythónica que los métodos getter y setter, para lo que se usan las propiedades.

¿Qué son exactamente las propiedades?

Las propiedades son atributos que manejamos mediante métodos getter, setter y deleter, por lo que podríamos llamarlos "atributos manejados". Podemos considerarlos unos atributos "especiales". En realidad en Python los datos, métodos y propiedades de una clase son todos atributos. Los métodos serían atributos "llamables" y las propiedades atributos "personalizables". La única diferencia entre una propiedad y un atributo estándar es que las primeras pueden invocar código personalizado al ser obtenidas, modificadas o eliminadas. Las propiedades se asocian a los atributos de la clase y, como cualquiera de ellos, son heredados en subclases e instancias. Una propiedad maneja un solo y específico atributo, y siempre harán referencia a los métodos de la clase en la que estén definidas.
Por lo tanto, las propiedades parecen atributos estándar, pero al acceder a ellos se lanzan los métodos getter, setter o deleter correspondientes. Como éstos métodos reciben el argumento self, podremos acceder a todos los elementos de la clase o la instancia (sus atributos y métodos) y hacer uso de ellos.

¿Cuándo usar entonces las propiedades?

Respecto al uso desde el principio de simples métodos getter , setter y deleter, las propiedades nos aportan un código mas pythónico, siendo mas fácil la lectura y escritura de los atributos. La interfaz es mas homogénea ya que a todo se accede mediante el operador punto.

Respecto al uso de atributos estándar, las propiedades nos dan la posibilidad de incorporar código que se ejecute de forma dinámica cuando intentamos acceder a ellos (para obtenerlos, modificarlos o borrarlos). Imaginemos el caso comentado con anterioridad: tenemos hecho un programa donde accedemos a atributos estándar públicos. Posteriormente necesitamos que esos atributos estándar sean validados con unas determinadas condiciones. Mediante las propiedades lograremos nuestro objetivo sin modificar el formato del código ya escrito, algo que no lograríamos con los métodos.

¿Cómo crear las propiedades?

La función property(), integrada en el intérprete, nos permite canalizar la lectura o escritura de los atributos (además de interceptar el momento en el que son borrados o proporcionar documentación sobre ellos) mediante funciones o métodos. Su formato es el siguiente:

atributo_de_clase = property(fget=None, fset=None,fdel=None, doc)

Ninguno de los parámetros es obligatorio. Si no los pasamos su valor por defecto es None. La función fget() se encargará de interceptar la lectura del atributo, la fset() de hacerlo cuando se escriba, la fdel() a la hora de borrarlo y el argumento doc recibirá una cadena para documentar el atributo (si no lo recibe, se copia el docstring de fget(),que por defecto tiene valor None). Si alguna operación no está permitida(por ejemplo si intentamos borrar un atributo, algo no demasiado habitual, y no tenemos indicada fdel()) se lanzará una excepción. La función fget() devolverá el valor procesado del atributo y tanto fset() como fdel() devolverán None. La función property() devuelve un objeto de tipo propiedad que se asigna al nombre del atributo. Un primer ejemplo del uso de las propiedades podría ser el siguiente:

class Persona:
    def __init__(self, nombre):
        self.set_nombre(nombre)
    def get_nombre(self):
        try:
            print("Pedimos atributo:")
            return self.__nombre
        except AttributeError:
            print("Error. No existe el atributo indicado")
        except:
            print("Error al acceder al atributo")
    def set_nombre(self, nuevo_nombre):
        print("Asignamos el valor", nuevo_nombre,"al atributo 'nombre'")
        self.__nombre = nuevo_nombre
        return None
    def del_nombre(self):
        try:
            print("Borramos atributo", self.__nombre)
            del self.__nombre
        except AttributeError:
            print("Error. No existe el atributo que desea borrar")
        except:
            print("Error al intentar borrar el atributo")
        return None
    nombre = property(get_nombre, set_nombre, del_nombre, "Mi información")
def main():
    a = Persona("Pepe")
    a.nombre = "Juan"
    print(a.nombre)
    del a.nombre
    del a.nombre
    print(a.nombre)
    a.nombre = "Elena"
    print(a.nombre)
    print(help(Persona.nombre))
main()

Obtenemos la siguiente salida:

>>> 
Asignamos el valor Pepe al atributo 'nombre'
Asignamos el valor Juan al atributo 'nombre'
Pedimos atributo:
Juan
Borramos atributo Juan
Error. No existe el atributo que desea borrar
Pedimos atributo:
Error. No existe el atributo indicado
None
Asignamos el valor Elena al atributo 'nombre'
Pedimos atributo:
Elena
Help on property:
    Mi información
None
>>>

En él se ha creado una clase Persona con un atributo privado nombre y tres métodos de tipo getter, setter y deleter. Posteriormente se crea, mediante la función property(), la propiedad nombre vinculada a ellos, que reemplaza al atributo del mismo nombre. Se añade además una información de ayuda. Más adelante creamos una instancia de Persona a la que posteriormente cambiamos el atributo nombre, tras lo cual lo borramos. Al intentar  borrarlo de nuevo, generamos un error que manejamos. También se lanza un error manejado al intentar leer el atributo borrado. Es interesante ejecutar el código paso a paso para ver su funcionamiento exacto. En este ejemplo, por simplicidad, no hemos manejado posibles excepciones en el método setter, pero sería conveniente hacerlo en un código completo.

Si no quisiésemos contravenir uno de los principios del Zen de Python ("Debería haber una, y preferiblemente solo una, forma obvia de hacer las cosas") los métodos get_nombre(), set_nombre() y del_nombre() deberían ser privados (algo que nuevamente logramos colocando un doble guión bajo antes de su nombre).

Pero tampoco es ésta la forma mas pythónica para tratar con las propiedades, algo que se consigue mediante el uso de decoradores. Recordemos que un decorador es básicamente una función que "envuelve" a otra dotándola (al añadir código) de alguna funcionalidad extra. El formato es el siguiente:

def mifunción(argumentos):
    ...
mifunción = decorador(mifunción)

También podríamos ponerlo así:

@decorador
def mifunción(argumentos)
    ...

Podemos ahora usar la función property() como decorador para que se ejecute cuando queramos acceder a uno de los atributos.

class Miclase:
    def mi_atributo(self):
        ...
    mi_atributo = property(mi_atributo)

O también colocarlo de la siguiente manera:

class Miclase:
     @property
     def mi_atributo(self):
         ...

De esta manera lograríamos pasar mi_atributo como el primer argumento de la función property(), que es el que se usa cuando intentamos leer. El objeto propiedad tiene métodos getter, setter y deleter que asignan los métodos de acceso de la propiedad y que devuelven una copia de la propia propiedad. Los podemos, a su vez, usar para decorar métodos con el mismo nombre mi_atributo que usaremos en el momento en que intentamos modificar o borrar el atributo. Es un poco lioso, por lo que el código nos puede aclarar un poco las cosas:

class Persona:
    def __init__(self, nombre):
        self.nombre = nombre
    @property
    def nombre(self):
        "Documentación del atributo 'nombre' "
        try:
            print("Pedimos atributo:")
            return self.__nombre
        except AttributeError:
            print("Error. No existe el atributo indicado")
        except:
            print("Error al leer el atributo")
    @nombre.setter
    def nombre(self, nuevo_nombre):
        print("Asignamos el valor",nuevo_nombre," al atributo 'nombre'")
        self.__nombre = nuevo_nombre
        return None
    @nombre.deleter
    def nombre(self):
        try:
            print("Borramos atributo", self.__nombre)
            del self.__nombre
        except AttributeError:
             print("Error. No existe el atributo que desea borrar")
        except:
             print("Error al borrar atributo")
        return None
def main():
    a = Persona("Pepe")
    a.nombre = "Juan"
    print(a.nombre)
    del a.nombre
    del a.nombre
    print(a.nombre)
    a.nombre = "Jaime"
    print(a.nombre)
    print(help(Persona.nombre))
main()

Salida:

>>> 
Asignamos el valor Pepe al atributo 'nombre'
Valor del atributo 'nombre':
Pepe
Asignamos el valor Juan al atributo 'nombre'
Valor del atributo 'nombre':
Juan
Borramos atributo Juan
Error. No existe el atributo que desea borrar
Valor del atributo 'nombre':
Error. No existe el atributo indicado
None
Asignamos el valor Jaime al atributo 'nombre'
Valor del atributo 'nombre':
Jaime
Help on property:
    Documentación del atributo 'nombre'
None
>>>

En el código definimos en  __init__() el atributo nombre como público. Eso nos permitirá que se ejecute, al crear la instancia de Persona, el método setter asociado a su propiedad y por tanto añadir el código que creamos conveniente. Nuevamente, por simplicidad, no se ha añadido manejo de excepciones ni condiciones de validación en él. Mediante la función property() decoramos el método getter que tiene el mismo nombre que nuestro atributo, es decir, nombre. Una vez hecho ésto, podemos usar los métodos setter y deleter del objeto propiedad para decorar los métodos correspondientes, de nombre nombre. Esta si es la forma mas pythónica de tratar las propiedades.

Los métodos getter de las propiedades pueden ser muy útiles si necesitamos calcular atributos sobre la marcha teniendo como base otros atributos. O también si queremos almacenar el resultado de un cálculo complejo para que sucesivas peticiones de ese cálculo no lo realicen de nuevo, sino que recuperen el resultado previo. Un ejemplo heterodoxo del uso de todo ello  es el siguiente: imaginemos que queremos crear una clase que almacene uno o una serie de valores. Podemos tener  o un número(entero o real) o una lista de números (reales y/o enteros). Como condición de validación tendremos que el número o números esté/n entre 0 y 10 (inclusive). De no ser así asignaremos al número el valor 0. Hay en la clase una propiedad llamada que simula (mediante el uso del módulo time) una operación pesada computacionalmente aplicada a los datos. Simulamos que nos cuesta tres segundos calcularla y el resultado es 123.79382.  Lo almacenamos en transformada  por si hay una posterior petición de su cálculo. Otra propiedad llamada media calcula el promedio de los datos ya filtrados mediante la validación. Se comprueba que los datos de entrada son correctos y si no se lanza una excepción de tipo ValueError.

El código y su correspondiente salida serían algo así, aconsejando una ejecución paso a paso para observar su funcionamiento detalladamente:

import time
class MiClase:
   def __init__(self, valor=None):
       self.valor = valor
       self.__transformada = None
   @property
   def valor(self):
       return self.__valor
   @valor.setter
   def valor(self, mivalor):
       if isinstance(mivalor, (int, float)):
           if 0 <= mivalor <=10:
               self.__valor = mivalor
           else:
               self.__valor = 0
       elif isinstance(mivalor, list):
           res = []
           for midato in mivalor:
               if isinstance(midato, (int, float)):
                   if 0 <= midato <=10:
                       res.append(midato)
                   else:
                       res.append(0)
               else:
                   raise ValueError ("El dato introducido no es válido")
           self.__valor = res
       else:
           raise ValueError("El dato introducido no es válido")
       return None
    @property
    def transformada(self):
        if not self.__transformada:
            print("Calculando transformada...")
            time.sleep(3)
            self.__transformada = 123.79382
        else:
            print("Transformada en caché. Valor:")
        return self.__transformada
    @property
    def media(self):
        if isinstance(self.valor, list):
            return sum(self.__valor) / len(self.__valor)
        else:
            return "\nNo se puede calcular la media al no ser una lista"
def main():
    datos = [12.21, 8.68, -2, 7.77]
    print("Los datos introducidos son: ", datos)
    a = MiClase(datos)
    print("Los datos filtrados son: ", a.valor)
    print("La media de los datos filtrados es:", a.media)
    print(a.transformada)
    print(a.transformada)
main()
>>> 
Los datos introducidos son: [12.21, 8.68, -2, 7.77]
Los datos filtrados son: [0, 8.68, 0, 7.77]
La media de los datos filtrados es: 4.1125
Calculando transformada...
123.79382
Transformada en caché. Valor:
123.79382
>>> 

Como mejorar tu script fácilmente

Esta entrada apareció originalmente en inglés en mi blog.

Nos ha pasado a todos. Ese momento en el que descubres que sabes suficiente sobre un lenguage de programacion que quieres ponerlo en práctica y construir "algo", lo que sea.
Una de las mejores cosas de la comunidad de Python es no sólo su habilidad para construir cosas increíbles, sino también para compartirlas con todo el mundo, mejorando la comunidad en el proceso.

Sin embargo, llevo un tiempo fijándome en un patrón que se repite en algunos de estos proyectos. Seguro que has visto alguno así. Hablo de esos proyectos con 2 ó 3 componentes, donde el README tiene una pequeña descripción del proyecto, quizás un par de lineas explicando como ejecutar el proyecto, y frases del tipo, "Seguramente añadiré X o Y si tengo tiempo".

El caso es que muchos de estos proyectos son realmente interesantes, y tienen algún tipo de componentes que me gustaría usar sin tener que implementarlos yo mismo.

Te voy a mostrar 3 formas distintas de implementar uno de estos proyectos, cada una de ellas mejor (desde mi punto de vista) que la anterior:

Supongamos que queremos construir un script genial, donde la funcionalidad principal será que, dado un número entero por el usuario, realizará un calculo simple en base a ese entero, y devolverá el resultado.

Implementación 1

 
#!/usr/bin/env python

"""
Super awesome script
Asks the user for a number:
 - If the number is less or equal to 100, it returns the 1st tetration of the number (power of itself)
 - else, it returns the number squared
"""

__version__ = '0.1'

if __name__ == '__main__':

    while 1:
        user_number = input('Choose a number:\n') #raw_input() in python2
        if user_number.isdigit():
            user_number = int(user_number)
            break
        else:
            print('{} is not a valid number'.format(user_number))

    if user_number > 100:
        print(user_number**2)
    else:
        print(user_number**user_number)

Ésta suele ser la implementación de alquien que lleva poco tiempo en python. Funciona, pregunta al usuario por el input, realiza la operación, e imprime en pantalla el resultado.

Veo dos problemas en esta implementación:

1. No hay ningún tipo de separación entre la lógica de la interacción del usuario y la lógica del cálculo. Todo esta incluido en el mismo macro bloque. Pese a ser funcional, esta implementación hace que sea díficil el modificar o expandir este script (para hacerlo tendrías que leerte todo el código).

2. Estamos gestionando toda la validación por nuestra cuenta. Python tiene formas de hacer esto para que tú no te tengas que molestar en hacerlo :).

Para la siguiente implementación, usaremos el módulo mas simple de la libreria standard para trabajar con inputs del usuario, .

Implementación 2

 
#!/usr/bin/env python

"""
Super awesome script
Asks the user for a number:
 - If the number is less or equal to 100, it returns it to the power of itself
 - else, it returns the number squared
"""

import argparse

__version__ = '0.2'


if __name__ == '__main__':

    parser = argparse.ArgumentParser()
    parser.add_argument('--number', required=True, type=int,
                        help='number to perform calculation')
    values = parser.parse_args()
    user_number = values.number
    if user_number > 100:
        print(user_number**2)
    else:
        print(user_number**user_number)

En esta implementación hemos eliminado el problema #2 de la implementación anterior. En esta ocasión usamos argparse, de esta forma dejamos que la libreria estándar se encargue de la validación del input. Esta implementación no funciona a menos que el input sea válido.

Todavía tenemos el problema #1, la separación entre la lógica del input y la lógica primaria (la función de calculo).

En la siguiente implementación vemos como podemos arreglar esto.

Implementación 3

 
#!/usr/bin/env python

"""
Super awesome script
Asks the user for a number:
 - If the number is less or equal to 100, it returns it to the power of itself
 - else, it returns the number squared
"""

import argparse

__version__ = '0.3'



def calculation(number):
    """Performs awesome calculation"""
    if number > 100:
        return number**2
    else:
        return number**number

if __name__ == '__main__':

    parser = argparse.ArgumentParser()
    parser.add_argument('--number', required=True, type=int,
                        help='number to perform calculation')
    values = parser.parse_args()
    user_number = values.number
    calculation_result = calculation(user_number)
    print(calculation_result)

En esta implementación, hemos hecho dos cosas:

1. Hemos puesto la carga de la validación en un módulo bien mantenida como es argparse.
2. Hemos separado la lógica del input del usuario de la lógica del input de cálculo.

Éste último cambio tiene tres ventajas sobre #1 y #2.

- Ventaja 1: En primer lugar, si nos damos cuenta que por algún motivo queremos modificar el 100 por un 200, ahora podemos fácilmente modificar eso, sin tener que modificar ni leer todo el código. Siempre y cuando la función calculation siga teniendo los mismos inputs y outputs, el resto de código seguirá funcionando sin problemas.

- Ventaja 2: Otro efecto, y para mi el más significativo, es que si ahora yo leo este script que otra persona ha escrito, y me gusta tanto que quiero añadirlo a un proyecto mio, ¡ahora puedo importarlo sin problemas!.

En las implementacines #1 y #2, la única manera de usar el script era haciendo:

python calculation_script.py --number INTEGER

Ahora, en la implementación #3, tenemos una manera mucho mas útil de usar la lógica mas importante (la del cálculo). Si yo tengo otro script en el que quiero usar la funcion de cálculo, puedo usarla de la forma:

 
from calculation_script import calculation

number = 10
calculation_result = calculation(number)

¿Increíble, no? Simplemente haciendo una pequeña modificación a la estructura del proyecto, ahora cualquier persona se puede beneficiar del mismo.

- Ventaja 3: Supongamos que este simple proyecto empieza a crecer, más desarrolladores se interesan y empiezan a colaborar. El código empieza a crecer y alguien comenta que tendría sentido empezar a trabajar en el suite de testing. (si no sabes lo que es el testing, te recomiendo este artículo.)

Con la implementación #3, testear la funcionalidad de calculation es super fácil (gracias a /u/choffee en reddit por el apunte):

 
import pytest
from calculation_script import calculation

class TestCalculation:
    """Calculation function does funky things to number
    More above 100 than below
    """
    def test_zero():
        x = 0
        assert calculation(x) == 0

    def test_border():
        x = 100
        assert calculation(x) == 10000

    def test_one():
        x = 1
        assert calculation(x) == 1

Piensa en ello la próxima vez, no cuesta nada y hace que tu script sea mejor 🙂

Cómo crear extensiones en C para Python usando CFFI y numba

Introducción

En este artículo vamos a ver cómo crear extensiones en C para Python usando CFFI y aceleradas con numba. El proyecto CFFI ("C Foreign Function Interface") pretende ofrecer una manera de llamar a bibliotecas escritas en C desde Python de una manera simple, mientras que numba, como podéis leer en nuestro blog, es un compilador JIT para código Python numérico. Mientras que hay algo de literatura sobre cómo usar CFFI, muy poco se ha escrito sobre cómo usar funciones CFFI desde numba, una característica que estaba desde las primeras versiones pero que no se completó hasta hace cuatro meses. Puede parecer contradictorio mezclar estos dos proyectos pero en seguida veremos la justificación y por qué hacerlo puede abrir nuevos caminos para escribir código Python extremadamente eficiente.

Este trabajo ha surgido a raíz de mis intentos de utilizar funciones hipergeométricas escritas en C desde funciones aceleradas con numba para el artículo que estoy escribiendo sobre poliastro. El resultado, si bien no es 100 % satisfactorio aún, es bastante bueno y ha sido relativamente fácil de conseguir, teniendo en cuenta que partía sin saber nada de C ni CFFI hace tres días.

¿Por qué CFFI + numba?

Como decíamos CFFI y numba, aunque tienen que ver con hacer nuestros programas más rápidos, tienen objetivos bastante diferentes:

  • CFFI nos permite usar C desde Python. De este modo, si encontramos algún algoritmo que merece la pena ser optimizado, lo podríamos escribir en C y llamarlo gracias a CFFI.
  • numba nos permite acelerar código Python numérico. Si encontramos algún algoritmo que merece la pena ser optimizado, adecentamos un poco la función correspondiente y un decorador la compilará a LLVM al vuelo.

Continue reading

Reglas para refactorizar funciones lambda

Un gran ejercicio que podéis hacer de vez en cuando es revisar la documentación oficial de Python. La misma me parece increiblemente completa aunque también un poco anárquica o sin un guión mínimamente claro para seguir diferentes tópicos.

Hoy, revisando el HOWTO de programación funcional, casi al final del documento y sin llamar la atención, he encontrado la siguiente documentación para refactorizar funciones lambda sugerida por Fredrik Lundh. Las reglas que propone para la refactorización de las funciones lambda dicen lo siguiente:

  1. Escribe una función Lambda.
  2. Escribe un comentario explicando qué se supone que hace la función lambda.
  3. Estudia el comentario durante un rato y piensa un nombre que capture la esencia del comentario.
  4. Convierte la función lambda a una declaración def usando el nombre pensado en el anterior paso.
  5. Elimina el comentario.

😛

Feliz año 2016.

Curso de Python en la ETSIAE: 4ª edición

¡Ya vamos a por la cuarta edición de nuestro curso de Python para aeronáuticos! Esta vez durará 1512 horas y este es el temario:

  1. Introducción a la sintaxis de Python
  2. Uso del Notebook de IPython
  3. Arrays de NumPy
  4. Representación gráfica con matplotlib
  5. Análisis numérico con SciPy
  6. Cálculo simbólico con SymPy

Curso AeroPython

El curso se desarrollará en la sala II del centro de cálculo de la ETSI Aeronáutica y del Espacio de 17:3018:00 a 20:00 a lo largo de dos semanas, los días 3, 4, 5, 10, 11 y 12.

Continue reading

¡Curso AeroPython en la UC3M!

¡Tenemos el placer de anunciar un nuevo curso de Python aplicado a la ingeniería aeroespacial, esta vez organizado por el Departamento de Ingeniería Aeroespacial de la Universidad Carlos III de Madrid!

El curso durará 10 horas, será gratuito para los alumnos y el temario será el siguiente:

  1. Introducción a la sintaxis de Python
  2. Uso del notebook de Jupyter
  3. Arrays de NumPy
  4. Representación gráfica con matplotlib
  5. Análisis numérico con SciPy
  6. Cálculo simbólico con SymPy

El horario será de 13:00 a 15:00 los días 5 a 9 de octubre y las clases tendrán lugar en el aula 7.0.J03.

Cartel Curso Python UC3M

Continue reading

Introducción a Machine Learning con Python (Parte 2)

En la entrada anterior, Introducción a Machine Learning con Python (Parte 1), di unas pequeñas pinceladas sobre lo que es el Aprendizaje Automático con algunos ejemplos prácticos. Ahora vamos a adentrarnos en materia de un modo más estructurado viendo paso a paso algunas de las técnicas que podemos emplear en Python.

Podemos dividir los problemas de aprendizaje automático en dos grandes categorías (Pedregosa et al., 2011):

  • Aprendizaje supervisado, cuando el conjunto de datos viene con los atributos adicionales que queremos predecir. El problema puede clasificarse en dos categorías:
    • Regresión: los valores de salida consisten en una o más variables continuas. Un ejemplo es la predicción del valor de una casa en función de su superficie útil, número de habitaciones, cuartos de baños, etc.
    • Clasificación: las muestras pertenecen a dos o más clases y queremos aprender a partir de lo que ya conocemos cómo clasificar nuevas muestras. Tenemos como ejemplo el Iris dataset que ya mostramos en la entrada anterior
  • Aprendizaje no supervisado, cuando no hay un conocimiento a priori de las salidas que corresponden al conjunto de datos de entrada. En estos casos el objetivo es encontrar grupos mediante clustering o determinar una distribución de probabilidad sobre un conjunto de entrada.

Como vemos, en ambos casos el aprendizaje automático trata de aprender una serie de propiedades del conjunto de datos y aplicarlos a nuevos datos.

Ésta entrada se la vamos a dedicar al aprendizaje supervisado, acompañando cada una de las técnica que veamos con un Notebook de Jupyter.

Aprendizaje supervisado

Empezaremos por el principio, y lo más sencillo, que es ajustar los datos a una línea para pasar luego a ver diferentes modelos de clasificación en orden creciente de complejidad en subsiguientes entradas.

Sigue leyendo...

SciPy Latin America 2015 en Argentina: ¡manda tu charla!

Ya lo hemos comentado por Twitter algunas veces, pero para quienes aún no lo sepáis: del 20 al 22 de mayo se celebrará la SciPy Latin America 2015 en Posadas, Misiones (Argentina), y gracias a la generosa invitación de los organizadores tanto Kiko como el que os escribe iremos en calidad de speakers invitados. ¡Muchísimas gracias! 😀

SciPy Latin America 2015

Este evento surge después de dos años de celebrar conferencias sobre Python científico en Argentina, la primera en 2013 en Puerto Madryn, Chubut y la segunda en 2014 en Bahía Blanca, Buenos Aires. Este año los organizadores han sumado fuerzas con gente de Brasil para llevar adelante un evento mucho más ambicioso a nivel de todo Latinoamérica. Desde aquí no puedo más que quitarme el sombrero por semejante esfuerzo (que incluye personas entendiéndose en portugués y español 🙂 ) y estoy seguro de que la SciPyLA 2015 será todo un éxito.

El plazo para presentar propuestas se cierra el día 6 de abril, ¡así que no pierdas un minuto para mandar tu charla, taller o póster a la conferencia! En el llamado a propuestas de la SciPyLA 2015 tienes toda la información sobre qué tipo de formatos se aceptan. La conferencia es gratuita, abierta a todo el mundo y habrá charlas en español, inglés y portugués. Nosotros ya hemos mandado nuestras propuestas, ¿a qué esperas tú? 🙂

Para mí es una ocasión muy emocionante para conocer a gente con la que normalmente solo puedo hablar a través de las redes sociales, y a la vez un reconocimiento excepcional a la labor que llevamos haciendo ya tres años en Pybonacci. Tendremos el honor de coincidir con personas que han tenido un papel crucial en el desarrollo de la comunidad Python argentina, hispana en general y brasileña, como Damián Ávila, Raniere Silva, Martín Gaitán, Filipe Saraiva, Mariano Reingart, Celia Cintas o Manuel Kaufmann, nos reencontraremos con Juan Bautista después de dos años de cruzarnos en la PyConES 2013 y desvirtualizaremos a fans que nos siguen desde el primer día como Cristian Hernán Schmidt. Puedes ver la lista pública de asistentes a la SciPyLA 2015 en la web de la conferencia.

Cartel SciPyLA 2015

No te puedes perder este evento: manda ya tu charla, regístrate (que es gratis) y ¡nos vemos en Argentina! 😉

C elemental, querido numba

Volvemos al torneo del rendimiento!!!

Recapitulando. Un artículo sobre Cython donde conseguíamos mejoras de velocidad de código Python con numpy arrays de 40x usando Cython desembocó en mejoras de 70x usando numba. En esta tercera toma vamos a ver si con Cython conseguimos las velocidades de numba tomando algunas ideas de la implementación de JuanLu y definiendo una función un poco más inteligente que mi implementación con Cython (busca_min_cython9).

Preparamos el setup inicial.

import numpy as np
import numba

np.random.seed(0)

data = np.random.randn(2000, 2000)
JuanLu hizo alguna trampa usando un numpy array en lugar de dos listas y devolviendo el resultado usando numpy.nonzero. En realidad no es trampa, es pura envidia mía al ver que ha usado una forma más inteligente de conseguir lo mismo que hacía mi función original 😛
Usando esa implementación considero que es más inteligente tener un numpy array de salida por lo que el uso de np.nonzero sería innecesario y añadiría algo de pérdida de rendimiento si luego vamos a seguir trabajando con numpy arrays. Por tanto, la implementación de JuanLu eliminando el uso de numpy.nonzero sería:

def busca_min_np_jit(malla):
    minimos = np.zeros_like(malla, dtype=bool)
    _busca_min(malla, minimos)
    return minimos  # en lugar de 'return np.nonzero(minimos)'

@numba.jit(nopython=True)
def _busca_min(malla, minimos):
    for i in range(1, malla.shape[1]-1):
        for j in range(1, malla.shape[0]-1):
            if (malla[j, i] < malla[j-1, i-1] and
                malla[j, i] < malla[j-1, i] and
                malla[j, i] < malla[j-1, i+1] and
                malla[j, i] < malla[j, i-1] and
                malla[j, i] < malla[j, i+1] and
                malla[j, i] < malla[j+1, i-1] and
                malla[j, i] < malla[j+1, i] and
                malla[j, i] < malla[j+1, i+1]):
                minimos[i, j] = True
%timeit -n 100 busca_min_np_jit(data)
100 loops, best of 3: 33 ms per loop
Ejecutándolo 100 veces obtenemos un valor más bajo de 33.6 ms devolviendo un numpy.array de 1's y 0's con los 1's indicando la posición de los máximos.
La implementación original la vamos a modificar un poco para que devuelva lo mismo.

def busca_min(malla):
    minimos = np.zeros_like(malla)
    for i in range(1, malla.shape[1]-1):
        for j in range(1, malla.shape[0]-1):
            if (malla[j, i] < malla[j-1, i-1] and
                malla[j, i] < malla[j-1, i] and
                malla[j, i] < malla[j-1, i+1] and
                malla[j, i] < malla[j, i-1] and
                malla[j, i] < malla[j, i+1] and
                malla[j, i] < malla[j+1, i-1] and
                malla[j, i] < malla[j+1, i] and
                malla[j, i] < malla[j+1, i+1]):
                minimos[i, j] = 1

    return minimos
%timeit busca_min(data)
1 loops, best of 3: 3.4 s per loop
Los tiempos son similares a la función original y, aunque estamos usando más memoria, tenemos una mejora con numba que ya llega a los dos órdenes de magnitud (alrededor de 100x!!) y una función más usable para trabajar con numpy.

Vamos a modificar la opción Cython más rápida que obtuvimos para que se comporte igual que las de Numba y Python.
Primero cargamos la extensión Cython.

# antes cythonmagic
%load_ext Cython
Vamos a usar la opción annotate para ver cuanto 'blanco' tenemos y la nueva versión Cython la vamos a llamar busca_min_cython10.

%%cython --annotate
import numpy as np
from cython cimport boundscheck, wraparound

cpdef char[:,::1] busca_min_cython10(double[:, ::1] malla):
    cdef unsigned int i, j
    cdef unsigned int ii = malla.shape[1]-1
    cdef unsigned int jj = malla.shape[0]-1
    cdef char[:,::1] minimos = np.zeros_like(malla, dtype = np.int8)
    #minimos[...] = 0
    cdef unsigned int start = 1
    #cdef float [:, :] malla_view = malla
    with boundscheck(False), wraparound(False):
        for j in range(start, ii):
            for i in range(start, jj):
                if (malla[j, i] < malla[j-1, i-1] and
                    malla[j, i] < malla[j-1, i] and
                    malla[j, i] < malla[j-1, i+1] and
                    malla[j, i] < malla[j, i-1] and
                    malla[j, i] < malla[j, i+1] and
                    malla[j, i] < malla[j+1, i-1] and
                    malla[j, i] < malla[j+1, i] and
                    malla[j, i] < malla[j+1, i+1]):
                    minimos[i,j] = 1

    return minimos







Generated by Cython 0.22

+01: import numpy as np
  __pyx_t_1 = __Pyx_Import(__pyx_n_s_numpy, 0, -1); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_GOTREF(__pyx_t_1);
  if (PyDict_SetItem(__pyx_d, __pyx_n_s_np, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
/* … */
  __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_GOTREF(__pyx_t_1);
  if (PyDict_SetItem(__pyx_d, __pyx_n_s_test, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
 02: from cython cimport boundscheck, wraparound
 03: 
+04: cpdef char[:,::1] busca_min_cython10(double[:, ::1] malla):
static PyObject *__pyx_pw_46_cython_magic_de7594aedda59602146d5e749862b110_1busca_min_cython10(PyObject *__pyx_self, PyObject *__pyx_arg_malla); /*proto*/
static __Pyx_memviewslice __pyx_f_46_cython_magic_de7594aedda59602146d5e749862b110_busca_min_cython10(__Pyx_memviewslice __pyx_v_malla, CYTHON_UNUSED int __pyx_skip_dispatch) {
  unsigned int __pyx_v_i;
  unsigned int __pyx_v_j;
  unsigned int __pyx_v_ii;
  unsigned int __pyx_v_jj;
  __Pyx_memviewslice __pyx_v_minimos = { 0, 0, { 0 }, { 0 }, { 0 } };
  unsigned int __pyx_v_start;
  __Pyx_memviewslice __pyx_r = { 0, 0, { 0 }, { 0 }, { 0 } };
  __Pyx_RefNannyDeclarations
  __Pyx_RefNannySetupContext("busca_min_cython10", 0);
/* … */
  /* function exit code */
  __pyx_L1_error:;
  __Pyx_XDECREF(__pyx_t_1);
  __Pyx_XDECREF(__pyx_t_2);
  __Pyx_XDECREF(__pyx_t_3);
  __Pyx_XDECREF(__pyx_t_4);
  __Pyx_XDECREF(__pyx_t_5);
  __PYX_XDEC_MEMVIEW(&__pyx_t_6, 1);
  __pyx_r.data = NULL;
  __pyx_r.memview = NULL;
  __Pyx_AddTraceback("_cython_magic_de7594aedda59602146d5e749862b110.busca_min_cython10", __pyx_clineno, __pyx_lineno, __pyx_filename);

  goto __pyx_L2;
  __pyx_L0:;
  if (unlikely(!__pyx_r.memview)) {
    PyErr_SetString(PyExc_TypeError,"Memoryview return value is not initialized");
  }
  __pyx_L2:;
  __PYX_XDEC_MEMVIEW(&__pyx_v_minimos, 1);
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}

/* Python wrapper */
static PyObject *__pyx_pw_46_cython_magic_de7594aedda59602146d5e749862b110_1busca_min_cython10(PyObject *__pyx_self, PyObject *__pyx_arg_malla); /*proto*/
static PyObject *__pyx_pw_46_cython_magic_de7594aedda59602146d5e749862b110_1busca_min_cython10(PyObject *__pyx_self, PyObject *__pyx_arg_malla) {
  __Pyx_memviewslice __pyx_v_malla = { 0, 0, { 0 }, { 0 }, { 0 } };
  PyObject *__pyx_r = 0;
  __Pyx_RefNannyDeclarations
  __Pyx_RefNannySetupContext("busca_min_cython10 (wrapper)", 0);
  assert(__pyx_arg_malla); {
    __pyx_v_malla = __Pyx_PyObject_to_MemoryviewSlice_d_dc_double(__pyx_arg_malla); if (unlikely(!__pyx_v_malla.memview)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 4; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
  }
  goto __pyx_L4_argument_unpacking_done;
  __pyx_L3_error:;
  __Pyx_AddTraceback("_cython_magic_de7594aedda59602146d5e749862b110.busca_min_cython10", __pyx_clineno, __pyx_lineno, __pyx_filename);
  __Pyx_RefNannyFinishContext();
  return NULL;
  __pyx_L4_argument_unpacking_done:;
  __pyx_r = __pyx_pf_46_cython_magic_de7594aedda59602146d5e749862b110_busca_min_cython10(__pyx_self, __pyx_v_malla);
  int __pyx_lineno = 0;
  const char *__pyx_filename = NULL;
  int __pyx_clineno = 0;

  /* function exit code */
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}

static PyObject *__pyx_pf_46_cython_magic_de7594aedda59602146d5e749862b110_busca_min_cython10(CYTHON_UNUSED PyObject *__pyx_self, __Pyx_memviewslice __pyx_v_malla) {
  PyObject *__pyx_r = NULL;
  __Pyx_RefNannyDeclarations
  __Pyx_RefNannySetupContext("busca_min_cython10", 0);
  __Pyx_XDECREF(__pyx_r);
  if (unlikely(!__pyx_v_malla.memview)) { __Pyx_RaiseUnboundLocalError("malla"); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 4; __pyx_clineno = __LINE__; goto __pyx_L1_error;} }
  __pyx_t_1 = __pyx_f_46_cython_magic_de7594aedda59602146d5e749862b110_busca_min_cython10(__pyx_v_malla, 0); if (unlikely(!__pyx_t_1.memview)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 4; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __pyx_t_2 = __pyx_memoryview_fromslice(__pyx_t_1, 2, (PyObject *(*)(char *)) __pyx_memview_get_char, (int (*)(char *, PyObject *)) __pyx_memview_set_char, 0);; if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 4; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_GOTREF(__pyx_t_2);
  __PYX_XDEC_MEMVIEW(&__pyx_t_1, 1);
  __pyx_r = __pyx_t_2;
  __pyx_t_2 = 0;
  goto __pyx_L0;

  /* function exit code */
  __pyx_L1_error:;
  __PYX_XDEC_MEMVIEW(&__pyx_t_1, 1);
  __Pyx_XDECREF(__pyx_t_2);
  __Pyx_AddTraceback("_cython_magic_de7594aedda59602146d5e749862b110.busca_min_cython10", __pyx_clineno, __pyx_lineno, __pyx_filename);
  __pyx_r = NULL;
  __pyx_L0:;
  __PYX_XDEC_MEMVIEW(&__pyx_v_malla, 1);
  __Pyx_XGIVEREF(__pyx_r);
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}
 05:     cdef unsigned int i, j
+06:     cdef unsigned int ii = malla.shape[1]-1
  __pyx_v_ii = ((__pyx_v_malla.shape[1]) - 1);
+07:     cdef unsigned int jj = malla.shape[0]-1
  __pyx_v_jj = ((__pyx_v_malla.shape[0]) - 1);
+08:     cdef char[:,::1] minimos = np.zeros_like(malla, dtype = np.int8)
  __pyx_t_1 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 8; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_GOTREF(__pyx_t_1);
  __pyx_t_2 = __Pyx_PyObject_GetAttrStr(__pyx_t_1, __pyx_n_s_zeros_like); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 8; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_GOTREF(__pyx_t_2);
  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
  __pyx_t_1 = __pyx_memoryview_fromslice(__pyx_v_malla, 2, (PyObject *(*)(char *)) __pyx_memview_get_double, (int (*)(char *, PyObject *)) __pyx_memview_set_double, 0);; if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 8; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_GOTREF(__pyx_t_1);
  __pyx_t_3 = PyTuple_New(1); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 8; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_GOTREF(__pyx_t_3);
  PyTuple_SET_ITEM(__pyx_t_3, 0, __pyx_t_1);
  __Pyx_GIVEREF(__pyx_t_1);
  __pyx_t_1 = 0;
  __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 8; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_GOTREF(__pyx_t_1);
  __pyx_t_4 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 8; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_GOTREF(__pyx_t_4);
  __pyx_t_5 = __Pyx_PyObject_GetAttrStr(__pyx_t_4, __pyx_n_s_int8); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 8; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_GOTREF(__pyx_t_5);
  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
  if (PyDict_SetItem(__pyx_t_1, __pyx_n_s_dtype, __pyx_t_5) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 8; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;
  __pyx_t_5 = __Pyx_PyObject_Call(__pyx_t_2, __pyx_t_3, __pyx_t_1); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 8; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_GOTREF(__pyx_t_5);
  __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
  __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
  __pyx_t_6 = __Pyx_PyObject_to_MemoryviewSlice_d_dc_char(__pyx_t_5);
  if (unlikely(!__pyx_t_6.memview)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 8; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
  __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;
  __pyx_v_minimos = __pyx_t_6;
  __pyx_t_6.memview = NULL;
  __pyx_t_6.data = NULL;
 09:     #minimos[...] = 0
+10:     cdef unsigned int start = 1
  __pyx_v_start = 1;
 11:     #cdef float [:, :] malla_view = malla
 12:     with boundscheck(False), wraparound(False):
+13:         for j in range(start, ii):
  __pyx_t_7 = __pyx_v_ii;
  for (__pyx_t_8 = __pyx_v_start; __pyx_t_8 < __pyx_t_7; __pyx_t_8+=1) {
    __pyx_v_j = __pyx_t_8;
+14:             for i in range(start, jj):
    __pyx_t_9 = __pyx_v_jj;
    for (__pyx_t_10 = __pyx_v_start; __pyx_t_10 < __pyx_t_9; __pyx_t_10+=1) {
      __pyx_v_i = __pyx_t_10;
+15:                 if (malla[j, i] < malla[j-1, i-1] and
      __pyx_t_12 = __pyx_v_j;
      __pyx_t_13 = __pyx_v_i;
      __pyx_t_14 = (__pyx_v_j - 1);
      __pyx_t_15 = (__pyx_v_i - 1);
      __pyx_t_16 = (((*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_12 * __pyx_v_malla.strides[0]) )) + __pyx_t_13)) ))) < (*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_14 * __pyx_v_malla.strides[0]) )) + __pyx_t_15)) )))) != 0);
      if (__pyx_t_16) {
      } else {
        __pyx_t_11 = __pyx_t_16;
        goto __pyx_L8_bool_binop_done;
      }
+16:                     malla[j, i] < malla[j-1, i] and
      __pyx_t_17 = __pyx_v_j;
      __pyx_t_18 = __pyx_v_i;
      __pyx_t_19 = (__pyx_v_j - 1);
      __pyx_t_20 = __pyx_v_i;
      __pyx_t_16 = (((*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_17 * __pyx_v_malla.strides[0]) )) + __pyx_t_18)) ))) < (*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_19 * __pyx_v_malla.strides[0]) )) + __pyx_t_20)) )))) != 0);
      if (__pyx_t_16) {
      } else {
        __pyx_t_11 = __pyx_t_16;
        goto __pyx_L8_bool_binop_done;
      }
+17:                     malla[j, i] < malla[j-1, i+1] and
      __pyx_t_21 = __pyx_v_j;
      __pyx_t_22 = __pyx_v_i;
      __pyx_t_23 = (__pyx_v_j - 1);
      __pyx_t_24 = (__pyx_v_i + 1);
      __pyx_t_16 = (((*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_21 * __pyx_v_malla.strides[0]) )) + __pyx_t_22)) ))) < (*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_23 * __pyx_v_malla.strides[0]) )) + __pyx_t_24)) )))) != 0);
      if (__pyx_t_16) {
      } else {
        __pyx_t_11 = __pyx_t_16;
        goto __pyx_L8_bool_binop_done;
      }
+18:                     malla[j, i] < malla[j, i-1] and
      __pyx_t_25 = __pyx_v_j;
      __pyx_t_26 = __pyx_v_i;
      __pyx_t_27 = __pyx_v_j;
      __pyx_t_28 = (__pyx_v_i - 1);
      __pyx_t_16 = (((*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_25 * __pyx_v_malla.strides[0]) )) + __pyx_t_26)) ))) < (*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_27 * __pyx_v_malla.strides[0]) )) + __pyx_t_28)) )))) != 0);
      if (__pyx_t_16) {
      } else {
        __pyx_t_11 = __pyx_t_16;
        goto __pyx_L8_bool_binop_done;
      }
+19:                     malla[j, i] < malla[j, i+1] and
      __pyx_t_29 = __pyx_v_j;
      __pyx_t_30 = __pyx_v_i;
      __pyx_t_31 = __pyx_v_j;
      __pyx_t_32 = (__pyx_v_i + 1);
      __pyx_t_16 = (((*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_29 * __pyx_v_malla.strides[0]) )) + __pyx_t_30)) ))) < (*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_31 * __pyx_v_malla.strides[0]) )) + __pyx_t_32)) )))) != 0);
      if (__pyx_t_16) {
      } else {
        __pyx_t_11 = __pyx_t_16;
        goto __pyx_L8_bool_binop_done;
      }
+20:                     malla[j, i] < malla[j+1, i-1] and
      __pyx_t_33 = __pyx_v_j;
      __pyx_t_34 = __pyx_v_i;
      __pyx_t_35 = (__pyx_v_j + 1);
      __pyx_t_36 = (__pyx_v_i - 1);
      __pyx_t_16 = (((*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_33 * __pyx_v_malla.strides[0]) )) + __pyx_t_34)) ))) < (*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_35 * __pyx_v_malla.strides[0]) )) + __pyx_t_36)) )))) != 0);
      if (__pyx_t_16) {
      } else {
        __pyx_t_11 = __pyx_t_16;
        goto __pyx_L8_bool_binop_done;
      }
+21:                     malla[j, i] < malla[j+1, i] and
      __pyx_t_37 = __pyx_v_j;
      __pyx_t_38 = __pyx_v_i;
      __pyx_t_39 = (__pyx_v_j + 1);
      __pyx_t_40 = __pyx_v_i;
      __pyx_t_16 = (((*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_37 * __pyx_v_malla.strides[0]) )) + __pyx_t_38)) ))) < (*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_39 * __pyx_v_malla.strides[0]) )) + __pyx_t_40)) )))) != 0);
      if (__pyx_t_16) {
      } else {
        __pyx_t_11 = __pyx_t_16;
        goto __pyx_L8_bool_binop_done;
      }
+22:                     malla[j, i] < malla[j+1, i+1]):
      __pyx_t_41 = __pyx_v_j;
      __pyx_t_42 = __pyx_v_i;
      __pyx_t_43 = (__pyx_v_j + 1);
      __pyx_t_44 = (__pyx_v_i + 1);
      __pyx_t_16 = (((*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_41 * __pyx_v_malla.strides[0]) )) + __pyx_t_42)) ))) < (*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_malla.data + __pyx_t_43 * __pyx_v_malla.strides[0]) )) + __pyx_t_44)) )))) != 0);
      __pyx_t_11 = __pyx_t_16;
      __pyx_L8_bool_binop_done:;
      if (__pyx_t_11) {
+23:                     minimos[i,j] = 1
        __pyx_t_45 = __pyx_v_i;
        __pyx_t_46 = __pyx_v_j;
        *((char *) ( /* dim=1 */ ((char *) (((char *) ( /* dim=0 */ (__pyx_v_minimos.data + __pyx_t_45 * __pyx_v_minimos.strides[0]) )) + __pyx_t_46)) )) = 1;
        goto __pyx_L7;
      }
      __pyx_L7:;
    }
  }
 24: 
+25:     return minimos
  __PYX_INC_MEMVIEW(&__pyx_v_minimos, 0);
  __pyx_r = __pyx_v_minimos;
  goto __pyx_L0;

Vemos que la mayor parte está en 'blanco'. Eso significa que estamos evitando usar la C-API de CPython y la mayor parte sucede en C. Estoy usando typed memoryviews que permite trabajar de forma 'transparente' con numpy arrays.
Vamos a ejecutar la nueva versión 100 veces, de la misma forma que hemos hecho con Numba:

%timeit -n 100 busca_min_cython10(data)
100 loops, best of 3: 27.6 ms per loop
Wow, virtualmente obtenemos la misma velocidad entre Numba y Cython y dos órdenes de magnitud de mejora con respecto a la versión Python.

res_numba = busca_min_np_jit(data)
res_cython = busca_min_cython10(data)
res_python = busca_min(data)

np.testing.assert_array_equal(res_numba, res_cython)
np.testing.assert_array_equal(res_numba, res_python)
np.testing.assert_array_equal(res_cython, res_python)
Parece que el resultado es el mismo en todo momento

Probemos con arrays de menos y más tamaño.

data = np.random.randn(500, 500)
%timeit -n 3 busca_min_np_jit(data)
%timeit -n 3 busca_min_cython10(data)
%timeit busca_min(data)
3 loops, best of 3: 2.04 ms per loop
3 loops, best of 3: 1.75 ms per loop
1 loops, best of 3: 209 ms per loop
data = np.random.randn(5000, 5000)
%timeit -n 3 busca_min_np_jit(data)
%timeit -n 3 busca_min_cython10(data)
%timeit busca_min(data)
3 loops, best of 3: 216 ms per loop
3 loops, best of 3: 174 ms per loop
1 loops, best of 3: 21.6 s per loop
Parece que las distintas versiones escalan de la misma forma y el rendimiento parece, más o menos, lineal.

Conclusiones de este nuevo capítulo.

Las conclusiones que saco yo de este mano a mano que hemos llevado a cabo JuanLu (featuring Numba) y yo (featuring Cython):

  • Cython: Si te restringes a cosas sencllas, es relativamente sencillo de usar. Básicamente habría que optimizar bucles y, solo en caso de que sea necesario, añadir tipos a otras variables para evitar pasar por la C-API de CPython en ciertas operaciones puesto que puede tener un coste elevado en el rendimiento. Para cosas más complejas, a pesar de que sigue siendo más placentero que C se puede complicar un poco más (pero no mucho más, una vez que has entendido cómo usarlo).
  • Numba: Es bastante sorprendente lo que se puede llegar a conseguir con poco esfuerzo. Parece que siempre introducirá un poco de overhead puesto que hace muchas cosas entre bambalinas y de la otra forma (Cython) hace lo que le digamos que haga. También es verdad que muchas cosas no están soportadas, que los errores que obtenemos puede ser un poco crípticos y se hace difícil depurar el código. Pero a pesar de todo lo anterior y conociendo el historial de la gente que está detrás del proyecto Numba creo que su futuro será brillante. Por ejemplo, Numbagg es una librería que usa Numba y que pretende hacer lo mismo que bottleneck (una librería muy especializada para determinadas operaciones de Numpy), que usa Cython consiguiendo resultados comparables aunque levemente peores.

No sé si habrá algún capítulo más de esta serie... Lo dejo en manos de JuanLu o de cualquiera que nos quiera enviar un nuevo post relacionado.