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.

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!!

Joyitas en la stdlib: collections

Dentro de la biblioteca estándar de Python dispones de auténticas joyas, muchas veces ignoradas u olvidadas. Es por ello que voy a empezar un breve pero intenso recorrido por algunas piezas de arte disponibles de serie.

Módulo collections

Con la ayuda de este módulo puedes aumentar las estructuras de datos típicas disponibles en Python (listas, tuplas, diccionarios,...). Veamos algunas utilidades disponibles:

ChainMap

Solo Python 3. Actualízate!!

Dicho en bruto, es un conglomerado de diccionarios (también conocidos como mappings o hash tables).

Para que puede ser útil:

Ejemplo, imaginemos que tenemos un diccionario de configuración dict_a, que posee las claves a y b, y queremos actualizar sus valores con otros pares clave:valor que están en el diccionario dict_b, que posee las claves b y c. Podemos hacer:

from collections import ChainMap

dict_a = {'a': 1, 'b': 10}
dict_b = {'b': 100, 'c': 1000}

cm = ChainMap(dict_a, dict_b)
for key, value in cm.items():
    print(key, value)
a 1
c 1000
b 10

Hemos añadido el valor de la clave c de dict_b sin necesidad de modificar nuestro diccionario original de configuración dict_a, es decir, hemos hecho un 'cambio' reversible. También podemos 'sobreescribir' las claves que están en nuestro diccionario original de configuración, dict_b variando los parámetros del constructor:

cm = ChainMap(dict_b, dict_a)
for key, value in cm.items():
    print(key, value)
b 100
a 1
c 1000

Vemos que, además de añadir la clave c, hemos sobreescrito la clave b.

Los diccionarios originales están disponibles haciendo uso del atributo maps:

cm.maps
[{'b': 100, 'c': 1000}, {'a': 1, 'b': 10}]

Ejercicio: haced un dir de cm y un dir de dict_a y veréis que los atributos y métodos disponibles son parecidos.

Más información en este hilo de stackoverflow en el que me he basado para el ejemplo anterior (¿basar y copiar no son sinónimos?).

Counter

Permite contar ocurrencias de forma simple. En realidad, su funcionalidad se podría conseguir sin problemas con algunas líneas extra de código pero ya que lo tenemos, está testeado e implementado por gente experta vamos a aprovecharnos de ello.

En la documentación oficial hay algunos ejemplos interesantes y en github podéis encontrar unos cuantos más. Veamos un ejemplo simple pero potente, yo trabajo mucho con datos meteorológicos y uno de los problemas recurrentes es tener fechas repetidas que no deberían existir (pero pasa demasiado a menudo). Una forma rápida de buscar problemas de estos en ficheros y lanzar una alarma cuando ocurra lo que buscamos, sería:

from io import StringIO
from collections import Counter

virtual_file = StringIO("""2010/01/01 2.7
2010/01/02 2.2
2010/01/03 2.1
2010/01/04 2.3
2010/01/05 2.4
2010/01/06 2.2
2010/01/02 2.2
2010/01/03 2.1
2010/01/04 2.3
""")

if Counter(virtual_file.readlines()).most_common(1)[0][1] > 1:
    print('fichero con fecha repetida')
fichero con fecha repetida

namedtuple

A veces me toca crear algún tipo de estructura que guarda datos y algunos metadatos. Una forma simple sin crear una clase ad-hoc sería usar un diccionario. Un ejemplo simple sería:

import numpy as np
import datetime as dt
from pprint import pprint

datos = {
    'valores': np.random.randn(100),
    'frecuencia': dt.timedelta(minutes = 10),
    'fecha_inicial': dt.datetime(2016, 1, 1, 0, 0),
    'parametro': 'wind_speed',
    'unidades': 'm/s'
}

pprint(datos)
{'fecha_inicial': datetime.datetime(2016, 1, 1, 0, 0),
 'frecuencia': datetime.timedelta(0, 600),
 'parametro': 'wind_speed',
 'unidades': 'm/s',
 'valores': array([-3.02664796, -0.59492715, -1.36233816, -0.27333458,  0.34971592,
        1.43105631,  1.12980511,  0.49542105,  0.37546829,  1.37230197,
       -1.00757915,  1.39334713,  0.73904326,  0.01129817,  0.12431242,
        0.4388826 , -0.49561972, -0.9777947 ,  0.6009799 ,  0.89101799,
        0.48529884,  1.80287157,  1.56321415, -0.62089358, -2.22113341,
       -0.04751354,  0.89715794, -0.23252567,  0.2259216 ,  0.35214745,
       -1.50915239, -1.46547279, -0.4260315 ,  0.20851012,  1.60555432,
        0.4221521 , -1.03399518,  1.68276277,  0.5010984 ,  0.01294853,
       -0.80004557,  1.72141514, -1.38314354,  0.41374512,  0.32861028,
       -2.22385654,  0.80125671, -0.84757451,  0.66896035, -0.26901047,
       -0.06195842, -0.60743183, -0.15538184,  1.16314508, -0.42198419,
        0.61174838,  0.97211057, -1.19791368, -0.68773007,  2.96956504,
       -1.13000346, -0.24523032,  1.6312053 ,  0.77060561, -1.69925633,
       -0.31417013,  0.44196826, -0.59763569,  0.91595894,  1.47587324,
        0.5520219 , -0.62321715,  0.32543574, -1.26181508,  0.94623275,
       -0.25690824,  1.36108942,  0.15445091, -1.25607974,  0.50635589,
        0.65698443, -0.82418166, -0.34054522,  0.23511397, -1.5096761 ,
       -1.12291338, -1.82440698, -0.47433931, -1.86537903,  1.29256869,
        1.78898905,  0.72081117, -0.15169929, -1.24106944,  0.68920997,
        0.36932816, -1.15901835, -0.93990956,  0.37258685, -0.41316085])}

Lo anterior es simple y rápido pero usando una namedtuple dispongo de algo parecido con algunas cosas extra. Veamos un ejemplo similar usando namedtuple:

from collections import namedtuple

Datos = namedtuple('Datos', 'valores frecuencia fecha_inicial parametro unidades')

datos = Datos(np.random.randn(100), 
              dt.timedelta(minutes = 10),
              dt.datetime(2016, 1, 1, 0, 0),
              'wind_speed',
              'm/s')
print(datos)
Datos(valores=array([ 1.50377059, -1.48083897, -0.76143985,  0.15346996, -0.01094251,
        0.42117233,  1.07136364, -0.24586714,  1.2001748 ,  0.56880926,
        0.56959121,  0.63811853,  0.4621489 ,  1.06636058,  0.32129287,
        2.42264145, -1.25830559, -0.27102862,  2.04853711,  2.07166845,
       -0.27138347, -0.07075163, -0.43547714,  1.69140984,  2.57150371,
        0.80336641, -0.78767876, -2.22281324,  0.23112338, -0.0605485 ,
        0.58304378,  3.33116997, -1.1285789 , -0.2047658 , -0.39240644,
       -1.69724959, -0.0313781 , -0.22892613, -0.06029154, -0.32368036,
       -0.12969429,  1.06231438,  0.05429922, -1.12206555,  1.33383161,
        0.92582424,  0.51615352,  0.93188459,  0.65273332,  0.39108396,
        1.56345696, -0.33158622, -0.27455745,  0.69101563,  1.61244861,
        0.7961402 ,  0.38661924, -0.99864208, -0.10720116,  0.40919342,
       -0.43784138, -3.06455306,  1.69280852,  1.82180641,  0.03604298,
        0.17515747,  1.4370723 , -0.47437528,  1.14510249,  1.36360776,
        0.34575948, -0.14623582,  1.1048332 , -0.2266261 ,  1.34319382,
        0.75608216, -0.62416011, -0.27821722,  0.45365802, -0.98537653,
        0.20172051,  1.70476797,  0.55529542, -0.07833625, -0.62619796,
       -0.02892921, -0.07349236,  0.94659497,  0.20823509,  0.91628769,
       -1.14603843, -0.20748714,  1.13008222, -0.93365802, -0.48125316,
        0.45564591, -0.03136778, -0.86333962,  1.04590165, -0.51757806]), frecuencia=datetime.timedelta(0, 600), fecha_inicial=datetime.datetime(2016, 1, 1, 0, 0), parametro='wind_speed', unidades='m/s')

Ventajas que le veo con respecto a lo anterior:

  • Puedo acceder a los 'campos' o claves del diccionario usando dot notation
print(datos.valores)
[ 1.50377059 -1.48083897 -0.76143985  0.15346996 -0.01094251  0.42117233
  1.07136364 -0.24586714  1.2001748   0.56880926  0.56959121  0.63811853
  0.4621489   1.06636058  0.32129287  2.42264145 -1.25830559 -0.27102862
  2.04853711  2.07166845 -0.27138347 -0.07075163 -0.43547714  1.69140984
  2.57150371  0.80336641 -0.78767876 -2.22281324  0.23112338 -0.0605485
  0.58304378  3.33116997 -1.1285789  -0.2047658  -0.39240644 -1.69724959
 -0.0313781  -0.22892613 -0.06029154 -0.32368036 -0.12969429  1.06231438
  0.05429922 -1.12206555  1.33383161  0.92582424  0.51615352  0.93188459
  0.65273332  0.39108396  1.56345696 -0.33158622 -0.27455745  0.69101563
  1.61244861  0.7961402   0.38661924 -0.99864208 -0.10720116  0.40919342
 -0.43784138 -3.06455306  1.69280852  1.82180641  0.03604298  0.17515747
  1.4370723  -0.47437528  1.14510249  1.36360776  0.34575948 -0.14623582
  1.1048332  -0.2266261   1.34319382  0.75608216 -0.62416011 -0.27821722
  0.45365802 -0.98537653  0.20172051  1.70476797  0.55529542 -0.07833625
 -0.62619796 -0.02892921 -0.07349236  0.94659497  0.20823509  0.91628769
 -1.14603843 -0.20748714  1.13008222 -0.93365802 -0.48125316  0.45564591
 -0.03136778 -0.86333962  1.04590165 -0.51757806]
  • Puedo ver el código usado para crear la estructura de datos usando verbose = True. Usa exec entre bambalinas (o_O). Puedo ver que todas las claves se transforman en property's. Puedo ver que se crea documentación... MAGIA en estado puro!!!

(Si no quieres usar la keyword verbose = True puedes seguir teniendo acceso en un objeto usando obj._source)

Datos = namedtuple('Datos', 'valores frecuencia fecha_inicial parametro unidades', verbose = True)
from builtins import property as _property, tuple as _tuple
from operator import itemgetter as _itemgetter
from collections import OrderedDict

class Datos(tuple):
    'Datos(valores, frecuencia, fecha_inicial, parametro, unidades)'

    __slots__ = ()

    _fields = ('valores', 'frecuencia', 'fecha_inicial', 'parametro', 'unidades')

    def __new__(_cls, valores, frecuencia, fecha_inicial, parametro, unidades):
        'Create new instance of Datos(valores, frecuencia, fecha_inicial, parametro, unidades)'
        return _tuple.__new__(_cls, (valores, frecuencia, fecha_inicial, parametro, unidades))

    @classmethod
    def _make(cls, iterable, new=tuple.__new__, len=len):
        'Make a new Datos object from a sequence or iterable'
        result = new(cls, iterable)
        if len(result) != 5:
            raise TypeError('Expected 5 arguments, got %d' % len(result))
        return result

    def _replace(_self, **kwds):
        'Return a new Datos object replacing specified fields with new values'
        result = _self._make(map(kwds.pop, ('valores', 'frecuencia', 'fecha_inicial', 'parametro', 'unidades'), _self))
        if kwds:
            raise ValueError('Got unexpected field names: %r' % list(kwds))
        return result

    def __repr__(self):
        'Return a nicely formatted representation string'
        return self.__class__.__name__ + '(valores=%r, frecuencia=%r, fecha_inicial=%r, parametro=%r, unidades=%r)' % self

    def _asdict(self):
        'Return a new OrderedDict which maps field names to their values.'
        return OrderedDict(zip(self._fields, self))

    def __getnewargs__(self):
        'Return self as a plain tuple.  Used by copy and pickle.'
        return tuple(self)

    valores = _property(_itemgetter(0), doc='Alias for field number 0')

    frecuencia = _property(_itemgetter(1), doc='Alias for field number 1')

    fecha_inicial = _property(_itemgetter(2), doc='Alias for field number 2')

    parametro = _property(_itemgetter(3), doc='Alias for field number 3')

    unidades = _property(_itemgetter(4), doc='Alias for field number 4')


# Lo mismo de antes
print(datos._source)
from builtins import property as _property, tuple as _tuple
from operator import itemgetter as _itemgetter
from collections import OrderedDict

class Datos(tuple):
    'Datos(valores, frecuencia, fecha_inicial, parametro, unidades)'

    __slots__ = ()

    _fields = ('valores', 'frecuencia', 'fecha_inicial', 'parametro', 'unidades')

    def __new__(_cls, valores, frecuencia, fecha_inicial, parametro, unidades):
        'Create new instance of Datos(valores, frecuencia, fecha_inicial, parametro, unidades)'
        return _tuple.__new__(_cls, (valores, frecuencia, fecha_inicial, parametro, unidades))

    @classmethod
    def _make(cls, iterable, new=tuple.__new__, len=len):
        'Make a new Datos object from a sequence or iterable'
        result = new(cls, iterable)
        if len(result) != 5:
            raise TypeError('Expected 5 arguments, got %d' % len(result))
        return result

    def _replace(_self, **kwds):
        'Return a new Datos object replacing specified fields with new values'
        result = _self._make(map(kwds.pop, ('valores', 'frecuencia', 'fecha_inicial', 'parametro', 'unidades'), _self))
        if kwds:
            raise ValueError('Got unexpected field names: %r' % list(kwds))
        return result

    def __repr__(self):
        'Return a nicely formatted representation string'
        return self.__class__.__name__ + '(valores=%r, frecuencia=%r, fecha_inicial=%r, parametro=%r, unidades=%r)' % self

    def _asdict(self):
        'Return a new OrderedDict which maps field names to their values.'
        return OrderedDict(zip(self._fields, self))

    def __getnewargs__(self):
        'Return self as a plain tuple.  Used by copy and pickle.'
        return tuple(self)

    valores = _property(_itemgetter(0), doc='Alias for field number 0')

    frecuencia = _property(_itemgetter(1), doc='Alias for field number 1')

    fecha_inicial = _property(_itemgetter(2), doc='Alias for field number 2')

    parametro = _property(_itemgetter(3), doc='Alias for field number 3')

    unidades = _property(_itemgetter(4), doc='Alias for field number 4')


  • Puedo seguir obteniendo un diccionario (un OrderedDict, también incluido en el módulo collections) si así lo deseo:
datos._asdict()['valores']
array([ 1.50377059, -1.48083897, -0.76143985,  0.15346996, -0.01094251,
        0.42117233,  1.07136364, -0.24586714,  1.2001748 ,  0.56880926,
        0.56959121,  0.63811853,  0.4621489 ,  1.06636058,  0.32129287,
        2.42264145, -1.25830559, -0.27102862,  2.04853711,  2.07166845,
       -0.27138347, -0.07075163, -0.43547714,  1.69140984,  2.57150371,
        0.80336641, -0.78767876, -2.22281324,  0.23112338, -0.0605485 ,
        0.58304378,  3.33116997, -1.1285789 , -0.2047658 , -0.39240644,
       -1.69724959, -0.0313781 , -0.22892613, -0.06029154, -0.32368036,
       -0.12969429,  1.06231438,  0.05429922, -1.12206555,  1.33383161,
        0.92582424,  0.51615352,  0.93188459,  0.65273332,  0.39108396,
        1.56345696, -0.33158622, -0.27455745,  0.69101563,  1.61244861,
        0.7961402 ,  0.38661924, -0.99864208, -0.10720116,  0.40919342,
       -0.43784138, -3.06455306,  1.69280852,  1.82180641,  0.03604298,
        0.17515747,  1.4370723 , -0.47437528,  1.14510249,  1.36360776,
        0.34575948, -0.14623582,  1.1048332 , -0.2266261 ,  1.34319382,
        0.75608216, -0.62416011, -0.27821722,  0.45365802, -0.98537653,
        0.20172051,  1.70476797,  0.55529542, -0.07833625, -0.62619796,
       -0.02892921, -0.07349236,  0.94659497,  0.20823509,  0.91628769,
       -1.14603843, -0.20748714,  1.13008222, -0.93365802, -0.48125316,
        0.45564591, -0.03136778, -0.86333962,  1.04590165, -0.51757806])
  • Puedo crear subclases de forma simple para añadir funcionalidad. Por ejemplo, creamos una nueva clase con un nuevo método que calcula la media de los valores:
class DatosExtendidos(Datos):
    def media(self):
        "Calcula la media de los valores."
        return self.valores.mean()

datos_ext = DatosExtendidos(**datos._asdict())

print(datos_ext.media())
0.27764229179

deque

Otra joyita que quizá debería usar más a menudo sería deque. Es una secuencia mutable (parecido a una lista), pero con una serie de ventajas. Es una cola/lista cuyo principio y fin es 'indistinguible', es thread-safe y está diseñada para poder insertar y eliminar de forma rápida en ambos extremos de la cola (ahora veremos qué significa todo esto). Un uso evidente es el de usar, por ejemplo, una secuencia como stream de datos con un número de elementos fijo y/o rápidamente actualizable:

  • Podemos limitar su tamaño y si añadimos elementos por un lado se eliminan los del otro extremo.
  • Podemos rotar los datos de forma eficiente.
  • ...

Veamos un ejemplo:

from collections import deque

dq = deque(range(10), maxlen = 10)
lst = list(range(10))
print(dq)
print(lst)
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# los tres últimos elementos los anexa nuevamente al principio de la secuencia.
dq.rotate(3)
print(dq)

lst = lst[-3:] + lst[:-3]
print(lst)
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
[7, 8, 9, 0, 1, 2, 3, 4, 5, 6]

Veamos la eficiencia de esta operación:

tmp = deque(range(100000), maxlen = 100000)
%timeit dq.rotate(30000)
tmp = list(range(100000))
%timeit tmp[-30000:] + tmp[:-30000]
The slowest run took 9.62 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 519 ns per loop
100 loops, best of 3: 3.07 ms per loop

Con una queue podemos anexar de forma eficiente a ambos lados:

dq.append(100)
print(dq)
dq.appendleft(10000)
print(dq)
deque([8, 9, 0, 1, 2, 3, 4, 5, 6, 100], maxlen=10)
deque([10000, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
dq.extend(range(10))
print(dq)
dq.extendleft([10, 100])
print(dq)
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
deque([100, 10, 0, 1, 2, 3, 4, 5, 6, 7], maxlen=10)

Etc.

Puedes hacer cosas similares a las hechas con listas pero de forma más eficiente y práctica en determinados casos!!

Recordad que, además, disponemos del módulo queue en la librería estándar.

Conclusión

Este módulo esconde cosas muy interesantes, algunas que no hemos visto. Por tanto, si no lo conocéis, deberíais explorar el módulo collections, si lo conocéis nos podéis indicar como lo usáis en los comentarios que puedes encontrar más abajo.