¿Cómo crear un mapa interactivo con Folium?

En esta entrada voy a describir el proceso usado para crear https://kikocorreoso.github.io/datos_aemet/ con la ayuda de la librería Folium.

¿Qué es Folium?

Folium es una librería Python que permite crear mapas interactivos usando Leaflet.js. Lo que hace, de forma elegante, es crear código javascript que usa la maravillosa librería de mapas interactivos leaflet.

¿Crear el mapa?

En la rama gh-pages del repositorio git datos_aemet en github (https://github.com/kikocorreoso/datos_aemet/tree/gh-pages) hay una serie de ficheros. Los ficheros index.html, map.html y readme.html los generaremos a partir de los ficheros:

  • custom.css: Algo de css para que la página cuadre. Por debajo usa, además, bootstrap.
  • src/create_base_map.py: Esta es la madre del cordero y lo que vamos a comentar.
  • src/template.html: Aquí tenemos la estructura principal del HTML usado y que sirve de plantilla a index.html y readme.html.

El fichero src/create_base_map.py hace una serie de cosas:

  • Por un lado lee template.html y modifica una parte del mismo para crear readme.html. Lo importante ocurre de las líneas 11 a 53. Básicamente lee un texto identificativo que he dejado en template.html y lo reemplaza por código explicativo sobre la página.
  • Por otro lado, lee el fichero hdf5 de datos, aemet.h5 y de los datos diarios extrae los metadatos de la estación y las fechas de inicio y fin de los registros. Esta información se formatea e incluye en un marcador usando folium.Marker. Cada marcador tendrá un color en función del periodo de datos disponible. Si una estación tiene, por ejemplo, más de 50 años de registros se incluye en un grupo usando folium.FeatureGroup. Estos grupos de estaciones discretizadas por periodo de medidas se pueden manejar mediante un control que aparece en la parte superior derecha del mapa y que incluimos usando folium.LayerControl. Tanto los grupos de marcadores discretizados por periodos de medidas como el control de capas lo incluimos en el mapa creado usando folium.Map. Cuando tenemos todo colocadito guardamos el mapa en formato html. El guardado del mapa nos crea una página donde el mapa ocupa el 100% de la misma.
  • Por último, la página con el mapa la incluimos en index.html mediante un IFrame usando el mismo template.html que hemos usando para el readme.html.

Creación de un mapa en detalle

Primero los imports

import folium
import branca

branca sirve para ayudarnos a meter HTML en los popup de los marcadores. Sin ello solo he conseguido ver texto plano.

Creamos el mapa:

mi_mapa = folium.Map(location=(39.7, 2.2), zoom_start=8)

Indicamos donde estará centrado el mapa usando location y el nivel de zoom inicial.

Podéis guardar el mapa

mi_mapa.save("mapa.html")

y abrirlo en vuestro navegador favorito y veréis un bonito mapa centrado cerca de unas hermosas islas.

Pero este mapa está muy tímido sin mostrar muchas cosas. Vamos a crear varios marcadores:

# creamos el mapa de nuevo para partir de 0
mi_mapa = folium.Map(location=(39.7, 2.2), zoom_start=8)
# creamos 4 marcadores
marcador1 = folium.Marker(location=(40, 2.1))
marcador2 = folium.Marker(location=(40, 3.5))
marcador3 = folium.Marker(location=(39, 2.1))
marcador4 = folium.Marker(location=(39, 3.5))

Y los incluimos en el mapa y guardamos el mapa:

marcador1.add_to(mi_mapa)
marcador2.add_to(mi_mapa)
marcador3.add_to(mi_mapa)
marcador4.add_to(mi_mapa)
mi_mapa.save("mapa.html")

Si abrís el mapa veréis cuatro marcadores alrededor de la isla de Mallorca. Si pulsáis sobre los marcadores no harán nada.

Vamos a incluir información en un popup y vamos a cambiar el color de los iconos de los marcadores usando folium.Icon:

# creamos el mapa de nuevo para partir de 0
mi_mapa = folium.Map(location=(39.7, 2.2), zoom_start=8)
# La información de los popups la añadiremos usando branca
# La información solo será la posición del marcador
# os dejo a vosotros la innovación
html = "<p>Latitud: 40.0</p><p>Longitud: 2.1</p>"
iframe1 = branca.element.IFrame(html=html, width=500, height=300)
html = "<p>Latitud: 40.0</p><p>Longitud: 3.5</p>"
iframe2 = branca.element.IFrame(html=html, width=500, height=300)
html = "<p>Latitud: 39.0</p><p>Longitud: 2.1</p>"
iframe3 = branca.element.IFrame(html=html, width=500, height=300)
html = "<p>Latitud: 39.0</p><p>Longitud: 3.5</p>"
iframe4 = branca.element.IFrame(html=html, width=500, height=300)
# creamos 4 marcadores y añadimos la información del popup usando folium.Popup
# además, añadimos un icono que será de un color para los marcadores al este
# y de otro color para los marcadores del oeste.
marcador1 = folium.Marker(
    location=(40, 2.1),
    popup=folium.Popup(iframe1, max_width=500),
    icon=folium.Icon(color="black")
)
marcador2 = folium.Marker(
    location=(40, 3.5),
    popup=folium.Popup(iframe2, max_width=500),
    icon=folium.Icon(color="gray")
)
marcador3 = folium.Marker(
    location=(39, 2.1),
    popup=folium.Popup(iframe3, max_width=500),
    icon=folium.Icon(color="black")
)
marcador4 = folium.Marker(
    location=(39, 3.5),
    popup=folium.Popup(iframe4, max_width=500),
    icon=folium.Icon(color="gray")
)
# Añadimos los marcadores al mapa
marcador1.add_to(mi_mapa)
marcador2.add_to(mi_mapa)
marcador3.add_to(mi_mapa)
marcador4.add_to(mi_mapa)
# Y guardamos el mapa
mi_mapa.save("mapa.html")

Por último, vamos a modificar un poco todo esto para añadir los marcadores del este (grises) a una capa y los del oeste (negros) a otra capa y añadir, además, el control de capas. Añado, además, los imports del principio para tener un script completo que podéis modificar a vuestro gusto.

import folium
import branca

# creamos el mapa de nuevo para partir de 0
mi_mapa = folium.Map(location=(39.7, 2.2), zoom_start=8)
# La información de los popups la añadiremos usando branca
# La información solo será la posición del marcador
# os dejo a vosotros la innovación
html = "<p>Latitud: 40.0</p><p>Longitud: 2.1</p>"
iframe1 = branca.element.IFrame(html=html, width=500, height=300)
html = "<p>Latitud: 40.0</p><p>Longitud: 3.5</p>"
iframe2 = branca.element.IFrame(html=html, width=500, height=300)
html = "<p>Latitud: 39.0</p><p>Longitud: 2.1</p>"
iframe3 = branca.element.IFrame(html=html, width=500, height=300)
html = "<p>Latitud: 39.0</p><p>Longitud: 3.5</p>"
iframe4 = branca.element.IFrame(html=html, width=500, height=300)
# creamos 4 marcadores y añadimos la información del popup usando folium.Popup
# además, añadimos un icono que será de un color para los marcadores al este
# y de otro color para los marcadores del oeste.
marcador1 = folium.Marker(
    location=(40, 2.1),
    popup=folium.Popup(iframe1, max_width=500),
    icon=folium.Icon(color="black")
)
marcador2 = folium.Marker(
    location=(40, 3.5),
    popup=folium.Popup(iframe2, max_width=500),
    icon=folium.Icon(color="gray")
)
marcador3 = folium.Marker(
    location=(39, 2.1),
    popup=folium.Popup(iframe3, max_width=500),
    icon=folium.Icon(color="black")
)
marcador4 = folium.Marker(
    location=(39, 3.5),
    popup=folium.Popup(iframe4, max_width=500),
    icon=folium.Icon(color="gray")
)
# Creamos dos grupos para los marcadores
grp_este = folium.FeatureGroup(name='Este')
grp_oeste = folium.FeatureGroup(name='Oeste')
# Añadimos los marcadores AL GRUPO AL QUE CORRESPONDAN (NO AL MAPA)
marcador1.add_to(grp_oeste)
marcador2.add_to(grp_este)
marcador3.add_to(grp_oeste)
marcador4.add_to(grp_este)
# Y ahora añadimos los grupos al mapa
grp_este.add_to(mi_mapa)
grp_oeste.add_to(mi_mapa)
# Y añadimos, además, el control de capas
folium.LayerControl().add_to(mi_mapa)
# Y guardamos el mapa
mi_mapa.save("mapa.html")

Et voilà, tenemos un precioso mapa interactivo con mucha funcionalidad en unas pocas líneas de Python.

Fin del 'así se hizo' de https://kikocorreoso.github.io/datos_aemet/.

Microentradas: Unicode/látex en la consola IPython/notebook Jupyter

[Este es un TIL que vi el otro día. Lo dejo aquí por si otros no lo sabían y les resulta útil.]

En la consola de IPython o en el notebook Jupyter podéis usar unicode escribiendo símbolos látex.

Por ejemplo, si escribes lo siguiente (en la consola o en una celda del notebook):

In [1]: \alpha

y pulsáis después la tecla tab veréis que se transforma a su símbolo látex y lo podéis usar fácilmente en vuestro código.

El resultado sería algo como lo siguiente (antes y después de pulsar la tecla tab):

  • Antes:

  • Después:

Esto puede ser útil para scripts propios, demos, formación,..., pero os lo desaconsejo en código en producción o a compartir 😉

Saludos.

Instalando PostgreSQL en local en windows o linux usando conda

Para el que no lo sepa, podéis instalar PostgreSQL usando conda \o/

Vamos a hacer un tutorial paso a paso para poder instalarlo todo y dejarlo listo para trabajar desde Python de forma sencilla.

[A lo largo de todo el tutorial se indica si el código a usar es para windows o para linux. Si no se indica nada el código debería ser válido en ambos sistemas operativos.]

1. Creamos un entorno virtual usando conda e instalamos PostgreSQL.

Este paso es sencillo. Solo necesitáis tener instalado conda en vuestro equipo y una conexión a internet. Si no tenéis conda instalado podéis ir a la sección de enlaces, más abajo, para visitar la documentación de conda donde os indica como instalarlo. Con conda instalado, podemos añadir el canal de conda-forge (básico para poder extender la cantidad de paquetes disponibles además de los mantenidos oficialmente). Lo podéis añadir a vuestros canales de referencia mediante el siguiente código a ejecutar en la línea de comandos:

conda config --add channels conda-forge

Una vez hecho lo anterior, en vuestra línea de comandos, podéis escribir:

conda create --name pgenv postgresql python=3.6

Lo anterior nos crea un entorno virtual conda llamado pgenv con Python 3.6. Activamos el nuevo entorno que hemos creado escribiendo en la línea de comandos:

source activate pgenv # linux

activate pgenv # windows

Antes de poder usar PostgreSQL debemos hacer alguna cosita más. Veamos la siguiente sección.

2. Creando un cluster de BBDD

Una vez instalado PostgreSQL deberéis crear la carpeta de datos donde se guardarán las BBDD (a esto se le llama cluster en los docs de PostgreSQL).

Lo vamos a instalar en una carpeta que se llame data. La carpeta data la creará el comando si no existe pero fallará si ya existe y no está vacía. Por ello, para evitar problemas podemos crear la carpeta a mano donde deseemos para asegurarnos que la misma esté vacía y para asegurarnos que tenemos permisos de escritura en esa ubicación.

mkdir /ruta/hasta/data # linux
mkdir "C:\\ruta\\hasta\\data" # windows.

En linux, ubicaciones populares de esta carpeta data son:

/usr/local/pgsql/data
/var/lib/pgsql/data

Pero la podéis colocar donde queráis.

Y ahora vamos al comando en cuestión. Si estáis en linux podéis hacer:

initdb -D /usr/local/pgsql/data # linux

En windows es similar pero con una ruta aceptable para windows:

initdb -D "C:\\ruta\\hasta\\data" # windows

De forma alternativa podéis hacer:

pg_ctl -D /usr/local/pgsql/data initdb # linux

"pg_ctl" -D "C:\\ruta\\hasta\\data" initdb # windows

Es mejor usar, en general, pg_ctl ya que es el comando que usaremos para arrancar, parar,..., el servidor de BBDD por lo que será útil familiarizarnos con el mismo.

3. Arrancando el servidor de BBDD.

Podemos arrancar el servidor de BBDD usando:

pg_ctl -D /usr/local/pgsql/data start # linux

"pg_ctl" -D "C:\\ruta\\hasta\\data" start # windows

Si, además, queremos tener un fichero log con la información de lo que se vaya
haciendo podemos usar la opción -l:

pg_ctl -D /usr/local/pgsql/data -l fichero_log start # linux

"pg_ctl" -D "C:\\ruta\\hasta\\data" -l fichero_log start # windows

Y se creará un fichero de texto con la información llamado fichero_log en la
ubicación desde donde lanzamos el comando (o en la ruta que defináis si así queréis). Es recomendable usar esta opción si no queréis que toda la información se vaya mostrando en la línea de comandos y para tener un registro de lo que vamos haciendo.

El directorio de datos se crea con seguridad mínima (modo trust). Como vamos a trabajar en local y, generalmente, en un sistema monousuario o con usuarios en los que confiamos no vamos a prestar mucha atención a esto pero puedes leer más sobre ello aquí.

El usuario por defecto del sistema que hace la instalación de PostgreSQL (usando conda en este caso) es el que se puede usar para la base de datos.

4. Interactuando con la base de datos.

Podemos instalar también psycopg2, driver para comunicar Python con PostgreSQL, y pgcli, una línea de comandos con esteroides, lo que viene a ser IPython para la consola Python. Con nuestro entorno pgenv activado escribimos en la línea de comandos:

conda install pgcli psycopg2

Genial, ¡qué fácil todo!

Vamos a crear nuestra primera base de datos. Para ello deberemos tener el servidor de BBDD funcionando. En este caso, con el comando que hemos usado anteriormente, pg_ctl ... start, debería haber arrancado y lo siguiente debería funcionar sin dar problemas:

createdb dbtest

Lo anterior debería haber creado una base de datos llamada dbtest. Si no ha
habido ningún problema podríamos acceder con pgcli (o psql, el comando de serie que viene con PostgreSQL) haciendo:

pgcli dbtest # 'psql dbtest' en caso que no hayáis instalado pgcli

(si hemos entrado en pgcli o en psql podemos salir usando \q).

Ahora podríamos empezar a crear tablas e insertar datos pero, si os acordáis, hemos instalado psycopg2. Usémoslo para hacerlo desde Python.

El siguiente código va a crear una tabla llamada tabla y vamos a insertar una serie de filas. Lo podéis ejecutar desde la consola Python mismo:

import psycopg2

# tu_usuario en la siguiente línea debería ser tu usuario del sistema
conn = psycopg2.connect("dbname=dbtest user=tu_usuario")

cur = conn.cursor()

cur.execute(
    "CREATE TABLE tabla (id serial PRIMARY KEY, num integer, num_txt varchar);"
)
cur.execute(
    "INSERT INTO tabla (num, num_txt) VALUES (%s, %s)", 
    (1, "uno")
)
cur.execute(
    "INSERT INTO tabla (num, num_txt) VALUES (%s, %s)",
    (10, "diez")
)

conn.commit()

cur.close()
conn.close()

Lo que hace el código anterior es, básicamente:

  • se conecta a la base de datos que acabamos de crear, dbtest,
  • crea una tabla, llamada 'tabla',
  • mete varias filas de datos en esa nueva tabla y,
  • finalmente, cierra la conexión con la base de datos.

Desde la línea de comandos podemos usar pgcli para hacer una consulta, también desde python pero vamos a hacerlo con pgcli en este caso:

(salid de la consola Python usando exit() si todavía estáis dentro de la misma)

En la línea de comandos:

pgcli dbtest # 'psql dbtest' si no habéis instalado pgcli

Ya dentro de pgcli (o psql) podemos hacer una consulta SQL:

SELECT * FROM tabla

Y nos debería dar el siguiente resultado:

+------+-------+-----------+
|  id  |  num  |  num_txt  |
|------+-------+-----------|
|   1  |   1   |    uno    |
|   2  |   10  |    diez   |
+------+-------+-----------+
SELECT 2
Time: 0.008s

Salimos nuevamente de pgcli (o psql) usando:

\q

5. Administración y limpieza.

Si no queréis la base de datos y la deseáis eliminar podéis usar, desde la línea de comandos:

dropdb dbtest

Y la base de datos se borrará.

Vamos a apagar el servidor PostgreSQL para ver como se hace:

pg_ctl -D /usr/local/pgsql/data stop # linux

"pg_ctl" -D "C:\\ruta\\hasta\\data" stop # windows

Si lo quisiéramos volver a arrancar podemos hacer:

pg_ctl -D /usr/local/pgsql/data start # linux

"pg_ctl" -D "C:\\ruta\\hasta\\data" start # windows

Podéis ver las diferentes opciones del comando pg_ctl mediante:

pg_ctl --help

Y eso es todo. Una forma sencilla de usar PostgreSQL en local mediante conda.

6. Eliminar el entorno virtual y PostgreSQL de forma eficaz.

Eliminarlo todo sería tan sencillo como eliminar el entorno conda creado una vez que lo tengamos desactivado y que el servidor de BBDD este parado.

Para parar el servidor de BBDD hacemos en la línea de comandos:

pg_ctl -D /usr/local/pgsql/data stop # linux

"pg_ctl" -D "C:\\ruta\\hasta\\data" stop # windows

Para desactivar el entorno virtual hacemos desde la línea de comandos

source deactivate # linux

deactivate # windows

Y para borrar el entorno virtual pgenv hacemos, desde la línea de comandos:

conda-env remove --name pgenv

Si, además, queremos hacer limpieza general de conda, limpiar paquetes 'cacheados' y liberar espacio en disco podemos hacer desde la linea de comandos:

conda clean -pt

Finalmente, borramos la carpeta data que creamos al principio de esta entrada.

rm -fr /usr/local/pgsql/data # linux

rmdir "C:\\ruta\\hasta\\data" /s /q # windows

Enlaces.

Página oficial de Postgresql: https://www.postgresql.org/
Documentación oficial de Postgresql: https://www.postgresql.org/docs/
Documentación oficial de conda: https://conda.io/docs/
Por si queréis cambiar el usuario y/o password que usa conda en la instalación: https://stackoverflow.com/questions/15008204/how-to-check-postgres-user-and-password#15008311
Documentación oficial de psycopg2: http://initd.org/psycopg/docs/index.html
Página oficial de pgcli: https://www.pgcli.com/

Notas finales.

No se entra en profundidad en ninguna de las herramientas (PostgreSQL, pgcli, psycopg2) para mantener el tutorial lo más sencillo posible.

En los enlaces tenéis mucha más información para ampliar.

En ningún caso se presta atención al tema de seguridad, configuración en profundidad,..., de PostgreSQL ya que eso daría para unas cuantas entradas.

¡¡Disfruten lo instalado!!

Donaciones gracias a Packtpub

[Go to english version]

Hola a todos.

Durante la semana del 5 al 11 de junio (desde el próximo lunes al próximo domingo), desde PacktPub, nos han propuesto usar un enlace de afiliación que se usará para que donen 1$ (hasta 1000$) a la organización sin ánimo de lucro que les indiquemos.

¿Cómo funcionará la campaña y cómo nos podéis ayudar?

Os animamos a participar, os animamos a elegir a qué organización queréis que donemos y os animamos a descargaros los libros de PacktPub de la semana que viene usando nuestro link de afiliación. Shhhh, os adelantamos que habrá tres de los siete que tratarán sobre Python.

Muchas gracias a todos por participar.

Saludos.

P.D..: Aviso para navegantes, no sacamos nada de este acuerdo más que poder ayudar a la comunidad que elijáis.


English version

Hi,

During the week starting Monday, June 5, until next Sunday, PacktPub, has proposed us to promote an affiliation link that will be used to donate 1$ (up to 1000$) to the non-profit organization we prefer.

How works the campaing and how could you help us?

We encourage you to participate, to choose the organization and to download the free ebook using the affiliation link. Shhhh, thee out of the seven books will be about Python.

Thank you very much for participating.

Saludos.

PS: Pybonacci do not obtain any economical profit or earnings from Packtpub.

Revisión del libro “Python 3, Curso Práctico” de Alberto Cuevas

Nos han pedido una revisión de un nuevo libro sobre Python en español. El libro se titula 'Python 3, Curso Práctico' y lo ha escrito Alberto Cuevas Álvarez. Si seguís leyendo podréis ver cómo ganar una copia en papel del mismo ;-D

Primero de todo, algunas características del libro:

  • Título: Python 3, Curso Práctico.
  • Autor: Alberto Cuevas Álvarez
  • Año de edición: 2016
  • Nº páginas: 560
  • ISBN: 978-84-9964-658-9
  • Editorial: RA-MA EDITORIAL
  • Encuadernación: Rústica
  • Idioma: Español (de España).
  • Versión de Python usada en el libro: 3.3
  • Link: http://www.ra-ma.es/libros/PYTHON-3-CURSO-PRACTICO/94627/978-84-9964-658-9
  • Precio: 31.90 € para la versión en papel.
  • Versión electrónica: No disponible de momento.

Le hemos pedido al autor que nos defina su intención al escribir el libro y esta es la frase resumen que nos ha enviado:

"Mi intención a la hora de realizar el libro ha sido explicar los fundamentos básicos de Python y las herramientas necesarias dentro de su ecosistema para conseguir crear aplicaciones gráficas completas en 2D ."

Como venimos haciendo, vamos a ir viendo el libro capítulo a capítulo para poder comentar de forma pormenorizada las pequeñas piezas que componen el mismo:

INTRODUCCIÓN

El libro empieza fuerte explicando, aunque sea muy por encima, cosas básicas de programación que pueden venir bien para fijar ciertos conceptos si no se dispone de cierto bagaje en ciencias de la computación.

EMPEZANDO A PROGRAMAR

En este punto se introducen conceptos importantes como lo que son las variables, cómo se asignan, ciertas funciones integradas (builtin) en el intérprete, operadores,etc. Cosas básicas introducidas en el momento oportuno. Está bien lo detallado de algún punto con múltiples ejemplos útiles. Se comenta la instalación de PyScripter, un IDE solo windows 🙁

ELEMENTOS FUNDAMENTALES DE PROGRAMACIÓN: INSTRUCCIÓN CONDICIONAL Y BUCLES

El capítulo habla de forma extensa del control de flujo, bucles y condiciones. Muy detallada la explicación de las condiciones. En este mismo capítulo se introducen algunas cosas que no tienen mucha relación como el mecanismo del import (explicado muy brevemente), el uso de números aleatorios (que no tiene mucha relación con el resto de contenidos del capítulo), depuración con PyScripter (menos útil para personas fuera de Windows),...

PROGRAMACIÓN FUNCIONAL

El título del capítulo es algo desafortunado ya que no estamos hablando sobre programación funcional sino sobre el uso de funciones en Python y esto podría confundir a alguien. El capítulo es muy completo para aprender a usar funciones en Python, el 'scope' de las variables, los parámetros y argumentos,... Nuevamente, algún ejemplo podría resultar confuso pero, en general, está bastante completo. Echo en falta que se nombre a las funciones lambda.

PROGRAMACIÓN ORIENTADA A OBJETOS

Llegamos a la parte de clases y la programación orientada a objetos (POO). El capítulo es bastante extenso y se explican muchas cosas, algunas de ellas avanzadas. Sin duda, este es el capítulo del libro que me gusta menos. Los ejemplos que se usan no son muy ortodoxos, nuevamente se recurre a casos muy particulares que provocan, siempre en mi modesta opinión, que algunas cosas resulten en más complejas de lo que deberían en este punto. Una buena parte del capítulo se habla sobre como se hacen algunas cosas con PyScripter lo que le resta valor a usuarios fuera de Windows.

TIPOS DE DATOS EN PYTHON

Este capítulo es muy detallado. Se explica con mucha profundidad los tipos básicos en Python, cadenas, listas, tuplas, conjuntos y diccionarios. Podría, incluso, servir como guía de referencia en español del uso de estos tipos  por lo detallado (son unas 90 páginas). Además, se usan muchísimos ejemplos para explicar los conceptos.

FICHEROS Y EXCEPCIONES

Se habla sobre cómo poder usar ficheros para leer y escribir información. Esta parte es muy detallada y extensa con múltiples ejemplos útiles. La parte de excepciones es más que correcta con un alcance adecuado para introducirlos. Por último, se muestra el uso de with pero se despacha en menos de una página por lo que me parece insuficiente. Este capítulo y el anterior son los que más me gustan del libro.

PROGRAMACIÓN GRÁFICA EN PYTHON MEDIANTE PYQT

No es normal encontrar una introducción a interfaces gráficas en un libro introductorio. Como mucho, se muestra un ejemplo básico para dejar al lector que se introduzca por su cuenta en el tópico si tiene interés. En este caso se hace una introducción mucho más extensa que el ejemplo típico por lo que si tienes interés en crear interfaces gráficas estás de suerte. Sin embargo, si no tienes mucho interés en ello, se van muchas páginas en ello. Se explican de forma detallada muchos de los widgets disponibles en PyQt y está muy enfocado a crear las interfaces usando Qt Designer.

GENERACIÓN DE GRÁFICOS EN PYTHON MEDIANTE MATPLOTLIB

Nuevamente, se introduce otra librería que quizá no sea de interés para todo el mundo. En alrededor de 50 páginas se habla sobre cómo instalar la librería, cómo usar pyplot con muchos ejemplos, como usar matplotlib usando POO y cómo integrar matplotlib en una aplicación PyQt. Reitero, si tienes interés en hacer gráficas tienes suerte pero si no es así se te van otras 50 páginas de libro en ello.

Apuntes varios sobre el libro:

Me gusta:

  • Lo detallado de la explicación del if en el capítulo 3.
  • Que no haya referencias a Python 2.
  • Lo detallado de la explicación de las funciones.
  • Lo detallado de la explicación de los tipos básicos de Python pudiendo servir, incluso, como guía de referencia en español.
  • La parte del tratamiendo de ficheros y excepciones se hace con un alto nivel de detalle.
  • La extensión del libro, que permite poder desarrollar algunos temas de forma muy completa.
  • Que sea en español.

No me gusta:

  • El uso de eval en muchos ejemplos del libro lo considero una mala práctica.
  • No se respeta el PEP8, enseñando malas prácticas, lo cual no me parece adecuado en un curso introductorio.
  • Algunos ejemplos usan casos extremos para explicar ciertos conceptos. Estos casos extremos pueden ser detalles de la implementación y no los considero adecuados ya que en lugar de ayudar pueden resultar más confusos.
  • Está muy enfocado a Windows (uso de PyScripter, instalaciones) haciendo que una buena parte de las páginas sea menos útil para gente que use otros sistemas operativos.

Conclusión

El libro tiene algunos altibajos con partes que brillan con luz propia y partes que mejoraría.

Si de mi dependiera me gustaría que algunas cosas se explicasen un poco mejor, como el uso de with, las funciones lambda, el mecanismo del import, la parte de POO. Metería partes con muchas librerías útiles de la librería estándar (math, itertools, datetime, os, sys, collections,...), como crear paquetes y módulos,... Reduciría o eliminaría los dos últimos capítulos.

Como comentario general, considero que el libro está bien pero, como todo en esta vida, se podría mejorar en algunos aspectos.

Sorteamos una copia entre nuestros lectores

El autor nos envió varias copias del libro. Una de ellas la vamos a sortear entre todos los lectores de Pybonacci residentes en España. Para participar:

  • Solo tienes que escribir un tweet indicando porqué te gustaría tener este libro incluyendo un enlace a http://www.ra-ma.es/libros/PYTHON-3-CURSO-PRACTICO/94627/978-84-9964-658-9
  • Una vez enviado el tweet nos lo enlazas en los comentarios de más abajo para que no se nos escape el tweet.
  • Si no tienes cuenta en twitter, déjanos un comentario más abajo indicando porqué te gustaría tener este libro.

Tenéis hasta el miércoles, 2 de noviembre a las 21:00:00 (CET) para participar en el sorteo. Después de pasada la fecha indicaremos cómo se hará el sorteo (actualizando algunas cosas de aquí), usando el número ganador del sorteo de la ONCE de ese día (2016/11/02) y anunciaremos el ganador.

Actualización: Resultado del sorteo

El número de la ONCE fue el 69907. Si introducís el código aquí:

sale que el ganador ha sido @FlixUjo. He usado los participantes por orden de fecha en su comentario, del más antiguo al más nuevo:

participantes = ['FlixUjo', 'Javier @runjaj', 'Antonio Molina',
'Raúl', 'José Carlos Juanos', 'Christian',
'Kike', 'Eduardo Campillos']

Enhorabuena al vencedor, por favor, mándanos un DM por twitter o usa el formulario de contacto para mandarnos una dirección de correo/teléfono o lo que prefieras.

Saludos a todos.

Joyitas en la stdlib: concurrent.futures

El módulo que vamos a ver, concurrent.futures, tiene una funcionalidad limitada. Trata de introducir una capa de simplificación sobre los módulos threading y multiprocessing.

Solo disponible en Python 3!!!!

Según la documentación oficial, el módulo proporciona una interfaz de alto nivel para ejecutar callables (¿invocables?) de forma asíncrona. Por su parte, según la Wikipedia, los futuros (no vamos a hablar de bolsa ni de especulación), en Programación, son un reemplazo para un resultado que todavía no está disponible, generalmente debido a que su cómputo todavía no ha terminado, o su transferencia por la red no se ha completado. El término futures también lo podéis encontrar como promises, delay, deferred,... En general, independientemente de cómo lo queráis llamar, lo podéis ver como un resultado pendiente.

El módulo posee una base abstracta, Executor, que se usa para las subclases ThreadPoolExecutor y ProcessPoolExecutor que, si sois un poco deductivos, sirven para usar multihilo (threading) o multiproceso (multiprocessing) por debajo, respectivamente.

Veamos un ejemplo de la primera subclase, ThreadPoolExecutor, con un caso práctico que hice el otro día. Tenía que descargar cosas y lo quise hacer asíncrono, es un ejemplo muy típico, pero, además, quisé mostrar en pantalla un mensaje dinámico para que se viese que el programa estaba 'haciendo algo' y que no se había quedado 'tostado'. Pongamos el ejemplo y luego lo comentamos y vemos el resultado:

 from concurrent.futures import ThreadPoolExecutor
from urllib import request
import itertools
from time import sleep

# Vamos a descargar algunas fotos de mi (abandonado) flickr.
# TODO: retomar la fotografía, hijos mediante.
pics = [
    'https://farm5.staticflickr.com/4117/4787042405_37e548cf3a_o_d.jpg', # Siria :_·(
    'https://farm3.staticflickr.com/2375/2457990042_e6d6982cb2_o_d.jpg', # Cuba, a ver si puedo volver para la ScipyLa'17
    'https://farm4.staticflickr.com/3149/3104818507_06cf582ba3_o_d.jpg', # Boston, amigos
    'https://farm3.staticflickr.com/2801/4084837185_4c12f32b1f_o_d.jpg', # Serengeti, una y mil veces
]

def tareas(pictures, workers=4):
    
    def get_pic(url):
        # No vamos a guardar las imágenes
        pic = request.urlopen(url).read()
        return pic
    
    msg = 'Descargando imágenes de https://www.flickr.com/photos/runbear1976 '
    ciclo = itertools.cycle('\|/-')
    executor = ThreadPoolExecutor(max_workers=workers)
    ex = [executor.submit(get_pic, url) for url in pictures]
    while not all([exx.done() for exx in ex]):
        print(msg + next(ciclo), end='\r')
        sleep(0.1)
    return ex

raw_data = tareas(pics, workers=4)
print()

Si ejecutáis el anterior código en una terminal, yo lo he llamado temp.py, podéis ver el efecto que andaba buscando:

futures

Vamos a comentar la función tareas un poco más en detalle.

  1. Dentro de esa función he escrito otra que se llama get_pic. Esa función lo único que hace es descargar la información bruta de las imágenes. Las imágenes no las vamos a guardar en nuestro disco duro ya que no es necesario.
  2. Luego vamos a crear msg y ciclo que serán el mensaje que mostraremos en pantalla. ciclo es un iterador infinito.
  3. Más tarde instanciamos ThreadPoolExecutor y creamos una lista, ex, donde guardar los 'futuros'. Los objetos instanciados de la clase Future (cada uno de los elementos de la lista ex) encapsulan la ejecución asíncrona del callable ('invocable'). Cada uno de estos objetos provienen de Executor.submit().
  4. Dentro del bloque while le preguntamos a cada una de las tareas si han terminado usando el método Future.done(). Si ha terminado nos devolverá True o, en caso contrario, nos devolverá False. Como quiero mostrar el mensaje de la imagen de más arriba mientras no hayan terminado todas las descargas en el bucle exijo que todas hayan terminado usando la función builtin all.
  5. Y, por último, devuelvo la lista con los 'futuros'.

En la función tareas podéis definir el número de workers a usar.

Si hacéis un print de raw_data, lo que devuelve la llamada a tareas, veréis que es algo parecido a lo siguiente:

 [<Future at 0x7fb50f324dd8 state=finished returned bytes>,
 <Future at 0x7fb50f324828 state=finished returned bytes>,
 <Future at 0x7fb50c08ed30 state=finished returned bytes>,
 <Future at 0x7fb50c08ea58 state=finished returned bytes>]

Si queréis la información bruta de una de las descargas podéis usar el método Future.result(). Si, además, volvéis a llamar al método Future.done() veréis ahora que os devuelve True, ya que está terminada la tarea.:

 futuro_x = raw_data[0]
print(futuro_x.result())
print(futuro_x.done())

Espero que la mini introducción os haya resultado de utilidad y de interés. Como siempre, si veis alguna incorrección, falta de ortografía,..., avisadnos en los comentarios.

Revisión del libro “Introducción a la programación con Python” de Nilo Menezes

En esta entrada vamos a hablar sobre el libro "Introducción a la programación con Python" escrito por Nilo Ney Coutinho Menezes.

El autor del libro se puso en contacto gentilmente con la "Pybonacci Crew" para hacer una revisión de su libro. Nosotros, como no podía ser de otra forma, hemos intentado colaborar agradeciendo su confianza.

Algunas generalidades del libro:

  • Escrito en español, cosa que se agradece dentro del desierto de recursos en español.
  • La calidad del libro en papel está muy bien y el diseño es bonito.
  • La primera edición en español (Julio/2016), que es la que hemos revisado, tiene un total de 336 páginas. En todo el libro no hay páginas en blanco para hacer que el libro sea más gordo, las partes de códigos de ejemplo del libro se saltan la parte relevante de saltos de línea del PEP-8 para que el libro sea más compacto incluso. Por tanto, tenemos un libro donde la mayor parte de las 336 páginas son páginas escritas y condensadas para que quepa incluso más en menos (less is more).
  • El libro es una traducción de un texto escrito en portugués/brasileño que ya va por su segunda edición y que se usa como texto introductorio en algunas universidades brasileñas.
  • Usa Python 3 en todo el libro (Python 3.4 para ser más precisos, aunque todo el código funcionará en Python 3.5 sin problemas).
  • La tabla de contenidos está aquí.
  • Su precio es inferior a los 25€.
  • Si vas a la PyConES podrás hablar con el autor en persona y pedirle que te ¡¡firme un ejemplar!!
  • No está disponible en formato electrónico.
Introducción a la programación con Python
Introducción a la programación con Python

Vamos a repasar un poco la estructura del libro capítulo a capítulo:

1. Motivación

El libro empieza con una declaración de intenciones sobre las motivaciones para aprender a programar y sobre porqué programar. Me parece una buena forma de empezar y de indicarle al lector que el camino que pretende empezar no es sencillo pero sí muy interesante y motivador y que le permitirá poder aprender a hacer cosas increibles que pueden incluso hacer cambiar el mundo (no sé si a mejor o a peor).

2. Preparando el ambiente

Aquí se hace una introducción muy detallada sobre como hacer funcionar (C)Python en tu sistema operativo paso a paso (sobre todo en el caso de Windows). Es obvio que es más detallado en Windows ya que en Linux o MacOS Python suele venir preinstalado. En el caso de Windows vienen pantallazos de todo el proceso. Además, enseña cómo usar el IDLE.

3. Variables y entrada de datos

En el capítulo 3 empezamos a programar en Python y a aprender código Python. Es una introducción muy sencilla a como usar variables y como introducir valores desde el intérprete.

4. Condiciones

Como era de esperar se explican un poco de control de flujo empezando por las condiciones y como ejecutar código si se cumplen determinados requerimientos. Nuevamente, la introducción a condiciones se hace de una forma simple y directa sin perdernos en casos extremos o singulares que nos harían perder el foco del aprendizaje.

5. Repeticiones

Todo el capítulo se dedica a hablar del uso de while y cómo romper el bucle y usar condiciones para realizar determinadas tareas. Se introducen conceptos como break.

6. Listas

En este momento se empieza a hablar de secuencias (listas, tuplas, diccionarios,...), sus métodos, cómo acceder a los elementos (indexación, clave),... Continuando con los bucles, que se introdujeron en el capítulo anterior, hay un pequeño apartado para hablar de for. Se habla de conceptos como pilas y colas de forma muy sencilla, se muestra un ejemplo del método de la burbuja para ordenación (bubble sort),... En fin, que mientras enseña tipos de datos muy comunes en programación, como por ejemplo las listas de python, enseña otros conceptos más generales de forma simple y amena.

Una cosa que no me gusta en exceso es que en este capítulo nombra el uso de objetos y métodos sin haber, siquiera, hablado sobre programación orientada a objetos. Un novato en programación no debería entender a qué se refiere en este momento cuando se usan palabras como 'objeto' y un pequeño inciso para nombrar lo que es la programación a objetos o la eliminación total de referencias a objetos y métodos quizá sería más adecuado. Sé que no es sencillo abstraerse totalmente de algunos conceptos e incluso obviarlos llegados a determinado punto. Aún así, el capítulo se entiende completamente sin grandes problemas.

7.Trabajando con cadenas de caracteres

Este es otro capítulo muy completo donde entramos en el mundo de las cadenas de caracteres (strings). Se habla del trabajo típico con cadenas y de la funcionalidad más básica como contar, sustituir, eliminar,..., caracteres. Además, se hace una gran y completa introducción a como formatear cadenas usando format (muchísimo más completo que el que hicimos nosotros aquí hace un tiempo). Acaba el capítulo programando un ejemplo del juego del ahorcado.

8. Funciones

El capítulo vuelve a ser una delicia de leer ya que introduce los diferentes términos poco a poco y de forma escalonada para ir de menos a más. Se habla de funciones lambda, del scope de las variables, de los argumentos, las palabras clave, argumentos opcionales,... Además, se meten otros conceptos como los módulos para ordenar nuestro código, empaquetamiento y desempaquetamiento de parámetros,...

 9. Archivos

En este momento se habla sobre cómo trabajar con archivos de texto. Un capítulo siempre necesario en cualquier texto introductorio. Nuevamente, se explica todo con sencillez y de forma ordenada yendo de menos a más. También se muestra como trabajar con directorios y archivos desde Python usando el módulo os.path. En este punto, se usan programas donde se incluye todo lo adquirido en el resto de capítulos de forma muy natural.

En este capítulo se usan ejemplos en los cuales se escriben documentos HTML los cuales los veo menos útiles que otros ejemplos típicos y, en mi opinión, más generales como usar un fichero json o un fichero csv,...

10. Clases y objetos

Ahora es cuando se muestra como usar clases y objetos. Se habla sobre herencia, métodos mágicos, sobrecarga de operadores,..., y se realiza un ejemplo muy extenso sobre una aplicación de una agenda.

En este momento considero que hay un salto muy abrupto en la curva de aprendizaje del libro. Hasta el capítulo 9 todo está explicado de forma detallada y paso a paso. En este momento se introduce la explicación del uso de clases y objetos y conceptos de programación orientada a objetos de forma que la curva de aprendizaje cambia su pendiente hablando de conceptos relativamente avanzados como la sobrecarga de operadores (sin nombrar el término), se introduce el uso de decoradores sin apenas explicación, se meten conceptos como super sin ninguna referencia previa,... El ejemplo de aplicación que se realiza en el capítulo pienso que es un poco avanzado y la explicación de los objetos que se hace está muy enlazada a cómo son los objetos en Python y no sobre hablar de objetos en general y de forma más abstracta.

11. Banco de datos

Aquí se habla sobre SQL y SQLite con Python. Se enseñan cosas básicas del lenguaje SQL y algunas cosas específicas del uso de SQL con SQLite y Python. Se ahonda en el ejemplo del capítulo anterior de la aplicación de una agenda cambiando algunas cosas para poder trabajar con una BBDD por debajo.

Este capítulo lo veo más accesorio y podría ser un apéndice o un capítulo en un libro más extenso y que entra en más profundidad en otra serie de conceptos. Dentro de un curso introductorio dónde se usa Python para aprender programación, el hecho de que se incluya un nuevo apartado con un nuevo lenguaje, SQL, con cosas específicas de SQLite no me resulta de lo más apropiado.

12. Próximos pasos

Se dan recomendaciones de por dónde seguir en este vasto mundo de la programación con enlaces a sitios y libros interesantes.

El capítulo es muy cortito (4 páginas) y se podría prescindir de él sin problemas.

Apéndice A. Mensajes de Error

Una pequeña revisión sobre los mensajes de error más típicos que nos podemos encontrar cuando empezamos a programar con Python.

Es muy útil tener una pequeña explicación sobre los errores más típicos que nos vamos a ir encontrando y cómo aprender a interpretarlos.

Apuntes varios sobre el libro:

Me gusta:

  • Lo detallado de las explicaciones en todos los capítulos excepto en los capítulos 10 y 11. En general, todo el libro es muy sencillo de seguir, todos los ejemplos están en formato digital para poderlos descargar. Me gusta especialmente la introducción a conceptos como pilas y colas, lo detallado del uso de format,...
  • Que en la página web dispones de varios vídeos para la parte más inicial del libro.
  • Que está en español.
  • Que use Python 3.
  • Que el precio del libro está bastante ajustado.
  • La predisposición del autor a introducir mejoras en futuras ediciones del libro.

No me gusta:

  • No me gusta que use el 'old style formatting' cuando en Python 3 se introdujo format y ahora se está introduciendo un nuevo formateo más potente incluso que format.
  • No me gusta que use unicode en la definición de variables, funciones, etcétera. Por ejemplo,
    def rectángulo(...):
        ...
    

    Esta práctica puede dar lugar a errores tontos por olvidar una tilde, puede dificultar el trabajo en equipos multiculturales,...

  • En general, la traducción es muy mejorable. El texto se puede entender sin problemas por cualquier hispanohablante pero la traducción se puede mejorar bastante y no haría deslucir a un gran libro de introducción a la programación.
  • El capítulo 10 es un poco errático y da un salto cuantitativo grande en lo que a complejidad se refiere.
  • El capítulo 11 es un capítulo que está bien tener pero que eliminaría del libro si el foco del autor es ponérselo lo más fácil posible a los que se están iniciando en un lenguaje de programación. Creo que desvía un poco el foco general del libro aunque los conceptos que se expliquen en ese capítulo sean extremadamente interesantes.
  • Me gustaría que todos los ejemplos del libro cumpliesen con el PEP-8. Cuando se está aprendiendo algo es cuando hay que introducir una serie de convenciones y buenas prácticas. Si se adquieren malos hábitos desde un principio luego es más difícil corregirlos.
  • Me gustaría que hubiera versión electrónica.

¿Compraría este libro?

Si se cumple que no sé leer en inglés y no tengo nociones de programación sin duda SÍ que compraría este libro, a pesar de algunas cosas que he puesto más arriba en el 'No me gusta'. Algunos de esos 'No me gusta' hacen deslucir un poco el libro pero más a nivel formal que a nivel general y técnico. Es un libro de buena calidad a un precio ajustado pero que se puede mejorar en distintos partes de forma sencilla.

Sorteamos una copia entre nuestros lectores

El autor nos envió tres copias del libro. Dos se han usado en los PyDay de Mallorca y de Madrid. El tercero lo vamos a sortear entre todos los lectores de Pybonacci que además tengan entrada para la PyConES de este año. Para participar:

  • Solo tienes que escribir un tweet indicando porqué te gustaría tener este libro incluyendo un enlace a https://librodepython.com/
  • Una vez enviado el tweet nos lo enlazas en los comentarios de más abajo para que no se nos escape el tweet.
  • Si no tienes cuenta en twitter, déjanos un comentario más abajo indicando porqué te gustaría tener este libro.

Tenéis hasta el miércoles, 21 de septiembre, a las 23:59:59 par participar en el sorteo. Al día siguiente se hará público el ganador y se entregará físicamente en la PyConES de Almería.

[Actualización] Anoche se cerró el plazo por lo que el ganador saldrá entre:

Sergio, Manuti, Miguel, Aníbal, Alberto y José.

Para saber quién ha ganado podréis meter el número ganador del sorteo de la ONCE de hoy (2016/09/22) en el formulario de más abajo.

Nos pondremos en contacto con el ganador en breve.

Saludos a todos.

Microentradas: Evitar ciertas etiquetas en la leyenda en Matplotlib

A veces, me llegan ficheros de datos con datos cada hora o cada día y los quiero representar en un plot. Para ello, podría acumular los ficheros en uno solo y luego pintarlo pero como lo debo hacer en 'tiempo casi-real' se puede meter todo en un bucle while que espera los ficheros cada hora/día/lo que sea y va pintando cada variable por tramos. Por ejemplo, una aproximación podría ser la siguiente:

import numpy as np
import matplotlib.pyplot as plt
plt.style.use('bmh')
%matplotlib inline

plt.figure(figsize = (12, 6))
for i in range(10):
    x = np.arange(i * 10, i * 10 + 10)
    y_var1 = np.random.randint(1, 5, 10)
    y_var2 = np.random.randint(5, 8, 10)
    plt.plot(x, y_var1, color = 'k', label = 'variable1')
    plt.plot(x, y_var2, color = 'g', label = 'variable2')
    plt.legend()
    plt.ylim(0, 9)

Como véis, en la gráfica anterior hay varios problemas pero como esta es una MicroEntrada solo nos vamos a centrar en el problema de las etiquetas repetidas en la leyenda.

¿Cómo podríamos evitar el meter tantas veces una etiqueta repetida?

Mi problema es que el bucle es o podría ser 'infinito' y tengo que inicializar las etiquetas de alguna forma. Si miro en esta respuesta encontrada en Stackoverflow dice que en la documentación se indica que "If label attribute is empty string or starts with “_”, those artists will be ignored." pero si busco aquí o en el enlace que indican en la respuesta en Stackoverflow no veo esa funcionalidad indicada en ningún sitio. Eso es porque aparecía en la versión 1.3.1 pero luego desapareció... Sin embargo podemos seguir usando esa funcionalidad aunque actualmente no esté documentada:

plt.figure(figsize = (12, 6))
for i in range(10):
    x = np.arange(i * 10, i * 10 + 10)
    y_var1 = np.random.randint(1, 5, 10)
    y_var2 = np.random.randint(5, 8, 10)
    plt.plot(x, y_var1, color = 'k', label = 'variable1' if i == 0 else "_esto_no_se_pintará")
    plt.plot(x, y_var2, color = 'g', label = 'variable2' if i == 0 else "_esto_tampoco")
    plt.legend()
    plt.ylim(0, 9)
Espero que a alguien le resulte útil.

Cómo llamar código C/C++ desde CPython (y Pypy) usando Cython y CFFI

Hace unas semanas surgió esta pregunta en StackOverflow en español: ¿Cómo llamar a código C++ desde Python?

Y la respuesta aceptada explica como hacer un wrapper sencillo usando Cython y CFFI. Como da la casualidad que la respuesta es mía voy a extenderla un poco para añadir más cosas y poder explicarla un poco mejor.

Prolegómenos

Antes de empezar a leer esta entrada deberías pasar a leer la entrada que hizo Juanlu hace un tiempo sobre CFFI titulada 'como crear extensiones en C para Python usando CFFI y Numba' donde se dan más detalles de todo el proceso a realizar con CFFI.

Antes de probar el código de la presente entrada deberías instalar cffi y cython:

conda install cffi cython # Válido en CPython

o

pip install cffi cython # Válido en CPython y Pypy

Todo lo que viene a continuación lo he probado en Linux solo usando CPython 3.5 y Pypy 5.1.1, compatible con CPython 2.7 e instalado usando esto.

Preliminares

Antes de pasar a la parte Cython y CFFI vamos a empezar creando los programas C/C++ que vamos a llamar desde Python.

Vamos a crear una librería que lo único que haga será sumar dos números enteros. Haremos una en C/C++ para Cython y una en C/C++ para CFFI.

C/C++ para Cython

C y C++ no son el mismo lenguaje pero para este caso el código se puede considerar el mismo. Para el caso C++ tendremos un fichero *.hpp y un fichero *.cpp (en C sería igual cambiando las extensiones a *.h y *.c, respectivamente).

El fichero *.hpp se llamará milibrería.hpp y contendrá el siguiente código:

long suma_enteros(long n, long m);

Mientras que el fichero *.cpp se llamará milibrería.cpp y contendrá el siguiente código:

long suma_enteros(long n, long m){
    return n + m;
}

Lo que hace el código es bastante simple.

C/C++ para CFFI

En este caso solo vamos a usar un fichero *.cpp y se llamará milibrería_cffi.cpp y contendrá el siguiente código:

long suma_enteros(long n, long m){
    return n + m;
}

extern "C"
{
    extern long cffi_suma_enteros(long n, long m)
    {
        return suma_enteros(n, m);
    }
}

El código es el mismo de antes más una segunda parte que nos permite hacer el código accesible desde Python.

Pegamento entre C/C++ y Python

En esta parte vamos a ver cómo unir el lenguaje compilado con el lenguaje interpretado.

Mediante Cython

Antes de nada necesitamos definir un fichero milibreria.pxd. Este fichero es parecido a lo que hacen los ficheros header en C/C++ o Fortran. Nos ayudará a 'encontrar' lo que hemos definido en c++ (más info sobre los ficheros pxd aquí):

cdef extern from "milibreria.hpp":
    long suma_enteros(long n, long m)

Un fichero *.pxd se puede importar en un fichero *.pyx usando la palabra clave cimport

Una vez 'enlazado' C/C++ con Cython mediante el fichero *.pxd necesitamos hacer que la parte C/C++ sea accesible desde Python. Para ello creamos el fichero pylibfromcpp.pyx, que es una especie de código Python un poco 'cythonizado' (cython es un superconjunto de Python):

cimport milibreria

def suma_enteros(n, m):
    return milibreria.suma_enteros(n, m)

Mediante CFFI

En este caso resulta un poco más sencillo, para este caso concreto. Hemos de crear el fichero Python que, mediante CFFI, enlazará C/C++ con Python. Este ficheros se llamará pylibfromCFFI.py y contendrá el siguiente código.:

import cffi


ffi = cffi.FFI()
ffi.cdef("long cffi_suma_enteros(long n, long m);")
C = ffi.dlopen("./milibreria.so")


def suma_enteros(n, m):
    return C.cffi_suma_enteros(n, m)

Setup

Compilando con Cython

Para poder acceder a la librería C/C++ hemos de crear un fichero setup.py que se encargará de la compilación que permitirá crear la extensión a la que accederemos desde Python. El fichero setup.py contendrá:

from distutils.core import setup, Extension
from Cython.Build import cythonize

ext = Extension("pylibfromcpp",
              sources=["pylibfromcpp.pyx", "milibreria.cpp"],
              language="c++",)

setup(name = "cython_pylibfromcpp",
      ext_modules = cythonize(ext))

Para crear la extensión en sí, en la misma carpeta donde hemos dejado todos los ficheros anteriores y desde la línea de comandos, hacemos (como siempre, recomiendo hacer esto desde un entorno virtual):

python setup.py build_ext -i

Y debería aparecer un fichero pylibfromcpp.cpp y otro fichero pylibfromcpp.pypy-41.so en la misma carpeta donde habéis ejecutado el comando anterior.

Compilando con CFFI

Para poder hacer accesible la funcionalidad definida en C/C++ desde Python podemos compilar usando:

g++ -o ./milibreria.so ./milibreria_cffi.cpp -fPIC -shared

Y deberíamos obtener el fichero milibreria.so.

Llamando desde Python

Usando nuestro 'wrapper' Cython

Ahora, si todo ha salido bien, dentro de un intérprete de python (como he comentado más arriba, lo he probado con CPython 3.5 y Pypy 5.1.1 y me ha funcionado en ambos) podemos hacer:

import pylibfromcpp
print(pylibfromcpp.suma_enteros(2, 3))

Usando nuestro 'wrapper' CFFI

De igual forma, si todo ha salido bien, podemos hacer:

import pylibfromcpp
print(pylibfromcpp.suma_enteros(2, 3))

Output completo en la consola pypy

Para el caso Cython

Python 2.7.10 (b0a649e90b6642251fb4a765fe5b27a97b1319a9, May 05 2016, 17:21:19)
[PyPy 5.1.1 with GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>> import pylibfromcpp
>>>> print(pylibfromcpp.suma_enteros(2, 3))
5

Para el caso CFFI

Python 2.7.10 (b0a649e90b6642251fb4a765fe5b27a97b1319a9, May 05 2016, 17:21:19)
[PyPy 5.1.1 with GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>> import pylibfromCFFI
>>>> print(pylibfromCFFI.suma_enteros(2, 3))
5

Comentarios finales

Un esquema, grosso modo, de lo que hemos hecho:

Esquema Cython - CFFI
Esquema Cython - CFFI

Pros y contras de cada una de las aproximaciones:

  • Cython permite usar Numpy sin problemas en CPython. Sin embargo, la última vez que intenté usar código Python con numpy arrays (Cython + Numpypy) reventaba todo en Pypy.
  • Cython lo podemos usar con CPython 2.x y 3.x. Cython funciona sin problemas en Pypy 5.1.1 (compatible con CPython 2.7). Numpypy NO funciona en Pypy3k.
  • El wrapper Cython que hemos hecho en este ejercicio es claramente más complejo que el que hemos hecho con CFFI (en este caso concreto).
  • Con Cython podemos usar el código compilado sin tocarlo mientras que con CFFI hemos de crear algo de código (muy simple) en el lenguaje compilado para acceder a su funcionalidad.
  • CFFI permite usar numpy arrays de forma sencilla, aunque, como con Cython, hay que 'ayudar con algo de código no Python' para que todo se pueda comunicar correctamente.

Documentación

Cython.

CFFI.

Joyitas en la stdlib: pathlib

El otro día estuvimos hablando de la biblioteca collections, una joya dentro de la librería estándar. Hoy vamos a hablar de una nueva biblioteca que se incluyó en la versión 3.4 de CPython llamada pathlib.

Solo python 3, actualízate!!!

Esta biblioteca nos da la posibilidad de usar clases para trabajar con las rutas del sistema de ficheros con una serie de métodos muy interesantes.

Algunas utilidades para configurar el problema

Vamos a crear un par de funciones que nos permiten crear y borrar un directorio de pruebas para poder reproducir el ejemplo de forma sencilla:

import os
import glob
import shutil
from random import randint, choice, seed
from string import ascii_letters

# función que nos crea un directorio de prueba en
# el mismo directorio del notebook
def crea_directorio():
    seed(1)
    base = os.path.join(os.path.curdir,
                        'pybonacci_probando_pathlib')
    os.makedirs(base, exist_ok = True)

    for i in range(0, randint(3, 5)):
        folder = ''.join([choice(ascii_letters) for _ in range(4)])
        path = os.path.join(base, folder)
        os.makedirs(path, exist_ok = True)
        for j in range(0, randint(2, 5)):
            ext = choice(['.txt', '.py', '.html'])
            name = ''.join([choice(ascii_letters) for _ in range(randint(5, 10))])
            filename = name + ext
            path2 = os.path.join(path, filename)
            open(path2, 'w').close()

# Función que nos permite hacer limpieza            
def borra_directorio():
    base = os.path.join(os.path.curdir,
                        'pybonacci_probando_pathlib')
    shutil.rmtree(base + os.path.sep)

Si ahora ejecutamos la función crea_directorio:

crea_directorio()

Nos debería quedar una estructura parecida a lo siguiente:

pybonacci_probando_pathlib/
├── KZWe
│   ├── CrUZoLgubb.txt
│   ├── IayRnBUbHo.txt
│   ├── WCEPyYng.txt
│   └── yBMWX.py
├── WCFJ
│   ├── GBGQmtsLFG.html
│   ├── PglOUshVv.py
│   └── RoWDsb.py
└── zLcE
    ├── AQlxJSXR.html
    ├── fCQGgXk.html
    └── xFUbEctT.html


Ejemplo usando lo disponible hasta hace poco

Pensemos en un problema que consiste en identificar todos los ficheros .py disponibles en determinada ruta y dejarlos en una nueva carpeta, que llamaremos python, todos juntos eliminándolos de la carpeta original en la que se encuentren.

De la forma antigua esto podría ser así:

# Suponemos que ya has creado los directorios y ficheros
# de prueba usando crea_directorio()

# recolectamos todos los ficheros *.py con sus rutas
base = os.path.join(os.path.curdir,
                    'pybonacci_probando_pathlib')
ficheros_py = glob.glob(os.path.join(base, '**', '*.py'))

# creamos la carpeta 'python' 
# dentro de 'pybonacci_probando_pathlib'
os.makedirs(os.path.join(base, 'python'), exist_ok = True)

# y movemos los ficheros a la nueva carpeta 'python'
for f in ficheros_py:
    fich = f.split(os.path.sep)[-1]
    shutil.move(f, os.path.join(base, 'python'))

Nuestra nueva estructura de ficheros debería ser la siguiente:

pybonacci_probando_pathlib/
├── KZWe
│   ├── CrUZoLgubb.txt
│   ├── IayRnBUbHo.txt
│   └── WCEPyYng.txt
├── python
│   ├── PglOUshVv.py
│   ├── RoWDsb.py
│   └── yBMWX.py
├── WCFJ
│   └── GBGQmtsLFG.html
└── zLcE
    ├── AQlxJSXR.html
    ├── fCQGgXk.html
    └── xFUbEctT.html

En el anterior ejemplo hemos tenido que usar las bibliotecas glob, os y shutil para poder realizar una operación relativamente sencilla. Esto no es del todo deseable porque he de conocer tres librerías diferentes y mi cabeza no da para tanto.

Limpieza

Me cargo la carpeta pybonacci_probando_pathlib para hacer un poco de limpieza:

borra_directorio()

Y vuelvo a crear la estructura de ficheros inicial:

crea_directorio()

Después de la limpieza vamos a afrontar el problema usando pathlib.

El mismo ejemplo con pathlib

Primero importamos la librería y, como bonus, creamos una función que hace lo mismo que la función borra_directorio pero usando pathlib, que llamaremos borra_directorio_pathlib:

from pathlib import Path

def borra_directorio_pathlib(path = None):
    if path is None:
        p = Path('.', 'pybonacci_probando_pathlib')
    else:
        p = path
    for i in p.iterdir():
        if i.is_dir():
            borra_directorio_pathlib(i)
        else:
            i.unlink()
    p.rmdir()

La anterior función con shutil es un poco más sencilla que con pathlib. Esto es lo único que hecho de menos en pathlib, algunas utilidades de shutil que vendrían muy bien de serie. Algo negativo tenía que tener.

En la anterior función, borra_directorio_pathlib, podemos ver ya algunas cositas de pathlib.

p = Path('.', 'pybonacci_probando_pathlib') nos crea una ruta que ahora es un objeto en lugar de una cadena. Dentro del bucle usamos el método iterdir que nos permite iterar sobre los directorios de la ruta definida en el objeto p. el iterador nos devuelve nuevos objetos que disponen de métodos como is_dir, que nos permite saber si una ruta se refiere a un directorio, o unlink, que nos permite eliminar el fichero o enlace. Por último, una vez que no tenemos ficheros dentro del directorio definido en p podemos usar el método rmdir para eliminar la carpeta.

Ahora veamos cómo realizar lo mismo que antes usando pathlib, es decir, mover los ficheros .py a la carpeta python que hemos de crear.

# recolectamos todos los ficheros *.py con sus rutas
p = Path('.', 'pybonacci_probando_pathlib')
ficheros_py = p.glob('**/*.py')

# creamos la carpeta 'python' dentro de 'pybonacci_probando_pathlib'
(p / 'python').mkdir(mode = 0o777, exist_ok = True)

# y copiamos los ficheros a la nueva carpeta 'python'
for f in ficheros_py:
    target = p / 'python' / f.name
    f.rename(target)

Nuevamente, nuestra estructura de ficheros debería ser la misma que antes:

pybonacci_probando_pathlib/
├── KZWe
│   ├── CrUZoLgubb.txt
│   ├── IayRnBUbHo.txt
│   └── WCEPyYng.txt
├── python
│   ├── PglOUshVv.py
│   ├── RoWDsb.py
│   └── yBMWX.py
├── WCFJ
│   └── GBGQmtsLFG.html
└── zLcE
    ├── AQlxJSXR.html
    ├── fCQGgXk.html
    └── xFUbEctT.html

Repasemos el código anterior:
Hemos creado un objeto ruta p tal como habíamos visto antes en la función borra_directorio_pathlib. Este objeto ahora dispone de un método glob que nos devuelve un iterador con lo que le pidamos, en este caso, todos los ficheros con extensión .py. En la línea (p / 'python').mkdir(mode = 0o777, exist_ok = True) podemos ver el uso de / como operador para instancias de Path. El primer paréntesis nos devuelve una nueva instancia de Path que dispone del método mkdir que hace lo que todos esperáis. Como ficheros_py era un iterador podemos usarlo en el bucle obteniendo nuevas instancias de Path con las rutas de los ficheros python que queremos mover. en la línea donde se define target hacemos uso del atributo name,que nos devuelve la última parte de la ruta. Por último, el fichero con extensión .py definido en el Path f lo renombramos a una nueva ruta, definida en target.

Y todo esto usando una única librería!!!

Echadle un ojo a la documentación oficial para descubrir otras cositas interesantes.

Si además de usar una única librería usamos parte de la funcionalidad de shutil tenemos una pareja muy potente, pathlib + shutil.

Limpieza II

Y para terminar, limpiamos nuestra estructura de ficheros pero usando ahora la función borra_directorio_pathlib que habíamos creado pero no usado aún:

borra_directorio_pathlib()

Notas

Ya hay un nuevo PEP relacionado y aceptado.

Enjoy!!