¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
  1. Libros
  2. Python 3
  3. Programación asíncrona: iniciación
Extrait - Python 3 Tratamiento de los datos y técnicas de programación
Extractos del libro
Python 3 Tratamiento de los datos y técnicas de programación
1 opinión
Volver a la página de compra del libro

Programación asíncrona: iniciación

Utilidad de la programación asíncrona

La búsqueda de rendimiento en algunos dominios, es un punto crucial. Como se ha visto anteriormente, la programación en paralelo, es decir, el uso de varias tareas o varios procesos, permite mejorar el rendimiento notablemente. Pero ellas no siempre son la solución más adaptada y sobre todo, hacen necesario la intervención de nociones de sistema y aumentan la complejidad de la aplicación. En algunos casos, cuando las causas de la falta de rendimiento de las aplicaciones se deben a la problemática de I/O, hay otra solución, que consiste en hacer programación asíncrona. La programación asíncrona permite evitar que una tarea se bloquee por una operación lenta que depende de un factor externo, como la recuperación de datos desde la red o el disco duro (lectura de archivos, consultas a bases de datos, consultas en Internet) o la escritura en red o el disco duro o incluso el uso de un periférico.

El inconveniente de la programación asíncrona, es que el código no se ejecuta más de manera totalmente predecible, lo que hace que la detección de errores sea un poco más compleja.

La asincronía es particularmente eficaz en muchas áreas, en particular en la Web, donde se ha convertido en algo muy de moda, con la aparición de, entre otros, Node.js, que permite tener...

Introducción a la asincronía

1. Noción de subrutina

Para este capítulo, el discurso se basará en la versión más reciente de Python 3, es decir, Python 3.7 en el momento de la redacción de este libro. El siguiente capítulo volverá sobre la sintaxis a utilizar en las versiones anteriores.

Para utilizar la programación asíncrona, hay que identificar en su código una parte que es bloqueante, es decir, que pasa tiempo de espera en las I/O.

Por lo tanto, hay que sacar esta parte del código lineal y hacer una subrutina.

Esta noción de subrutina está en la base de la programación asíncrona.

A continuación se muestra el ejemplo más básico, adaptado de la documentación oficial: 

>>> import asyncio  
>>> import time   
>>> async def main():  
...     print(f"hello {time.strftime('%X')}")  
...     await asyncio.sleep(1)  
...     print(f"world {time.strftime('%X')}")  
... 

La primera línea permite importar el módulo asyncio, que se centra en la problemática de programación asíncrona y la segunda el módulo time, que nos va a permitir visualizar el tiempo tomado realmente por los algoritmos.

La tercera línea recuerda a la definición de una función, salvo que la palabra clave def está precedida de la palabra clave async. Este sencillo detalle convierte a la función en una subrutina.

Para ser más precisos, en este ejemplo main es una función y la llamada a esta función main(), nos va a dar la subrutina.

>>> main  
<function main at 0x7f030738ec80>  
>>> main()  
<subroutine object main at 0x7f03073e99c8> 

Ahora vamos a intentar una subrutina con los argumentos:

>>> async def say_after(delay, what):  
...     print(f"before {what} {time.strftime('%X')}")  
...     await asyncio.sleep(delay)  
...     print(f"after  {what} {time.strftime('%X')}")  
... 

Este ejemplo...

Elementos de gramática

1. Administrador de contexto asíncrona

El administrador de contexto permite gestionar correctamente un recurso:

>>> with open("path/to/file") as f:  
...     content = f.read()  
... 

Procediendo de esta manera, sabemos qué sucede; nuestro archivo se cerrará correctamente. Y esto es aplicable a cualquier recurso, en particular a los recursos de red.

Sucede lo mismo para el administrador de contexto asíncrono, salvo que las herramientas asíncronas tratan el recurso de manera asíncrona.

En otros términos, hace necesario un tratamiento particular que es diferente al del administrador de contexto clásico, pero al final, esto es relativamente transparente para el desarrollador que solo tiene que añadir la palabra clave async, delante de la palabra clave with.

A continuación se muestra un ejemplo con el módulo aiofiles: https://github.com/Tinche/aiofiles:

>>> async with aiofiles.open("path/to/file") as f:  
...     content = await f.read()  
... 

A continuación se muestra un ejemplo con el módulo aiohttp, que permite gestionar los recursos HTTP de manera asíncrona, como su nombre indica:

>>> import asyncio  
>>> import aiohttp  
>>> async def download_json():  
...    ...

Nociones avanzadas

Esta sección se dirige a los lectores avanzados.

1. Introspección

En primer lugar, veremos lo que podemos encontrar por nosotros mismos, para entender qué es una tarea asíncrona:

>>> async def introspect():  
...     task = asyncio.current_task()  
...     print(task)  
...     print(type(task), type.mro(type(task)))  
...     print(dir(task))  
... 

Para recuperar la tarea asíncrona que se está ejecutando, se puede llamar a asyncio.current_task. Evidentemente, esto nos devolverá la subrutina introspect

Se lanza normalmente y se puede comprobar el primer punto: la tarea actual, dentro de nuestra tarea, es correctamente nuestra tarea:

>>> asyncio.run(introspect())  
<Task pending coro=<introspect()> 

Posteriormente, si observamos el árbol de herencia, veremos esto:

[<class '_asyncio.Task'>, <class '_asyncio.Future'>, <class 'object'>] 

Nuestra subrutina es un objeto del tipo _asyncio.task y es un tipo particular de futuros, uno de los tres tipos de awaitables (con la tarea y la subrutina).

Veamos ahora cuales son los atributos de la clase:

['__await__', '__class__', '__del__', '__delattr__', '__dir__', '__doc__', 
'__eq__', '__format__', '__ge__', '__getatributoe__', '__gt__', '__hash__', 
'__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', 
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__
sizeof__', '__str__', '__subclasshook__',  
'_asyncio_future_blocking', '_callbacks', '_coro', '_excepción', '_fut_waiter', 
'_log_destroy_pending', '_log_traceback', '_loop',  
'_must_cancel', '_repr_info', '_result', '_source_traceback', '_state',  
'add_done_callback', 'all_tasks', 'cancel', 'cancelled', 'current_task', 
'done', 'excepción', 'get_loop'...

Bucle orientado a eventos asíncrono

1. Gestión del bucle

Cualquier tarea asíncrona es ejecutada por medio de lo que llamamos, bucle orientado a eventos. Si la programación en paralelo se basa en la gestión de los hilos de ejecución o los procesos realizados por la máquina virtual y por el sistema, la programación asíncrona se basa en el bucle orientado a eventos proporcionados por asyncio para gestionar de la manera la más eficaz posible, la gestión de las I/O, de los eventos de sistema y de los cambios de contextos de aplicación.

La aplicación que desarrollamos va a interactuar con el bucle orientado a eventos, para registrar el código a ejecutar y dejar tomar las decisiones apropiadas para ordenar la ejecución de este código en el momento más apropiado, cuando los recursos solicitados son accesibles. Cada tarea, una vez que le damos el control, va a poder avanzar y después devolver el control al bucle, tan pronto como se ponga en espera.

En todo lo que hemos visto hasta ahora, la presencia del bucle orientado a eventos es totalmente invisible en el código, pero cada vez que se usa la palabra clave await, se vuelve a este bucle y esto va a elegir una nueva tarea asíncrona a ejecutar.

Para obtener el bucle asíncrono actual, se puede hacer:

>>> asyncio.get_running_loop() 

Atención, el bucle actual solo está disponible cuando estamos en una función asíncrona, ejecutada por ejemplo por asyncio.run.

Si no hay bucle asíncrono, entonces se obtiene una excepción:

>>> asyncio.get_running_loop()  
Traceback (most recent call last):  
  File "<input>", line 1, in <module>  
    asyncio.get_running_loop()  
RuntimeError: no running event loop 

Este bucle actual se utiliza con preferencia. Se gestiona automáticamente por asyncio.

Sepa que también es posible utilizar:

>>> loop = asyncio.get_event_loop() 

Este último comando puede funcionar fuera de una función asíncrona y se utiliza mucho. Por lo tanto, lo verá habitualmente en los ejemplos.

Es posible crear un nuevo bucle:

>>> loop = asyncio.new_event_loop() 

Se puede declarar este bucle como el bucle actual, de la siguiente manera:

>>> asyncio.set_event_loop(loop)...

Utilizar el bucle orientado a eventos

1. Utilización de las funciones de retorno (callbacks)

El rol del retorno de función (callback en inglés), consiste en permitir realizar una operación que queremos hacer antes incluso de que el futuro tenga un resultado. Por lo tanto, se programa esta operación antes y se ejecuta después de que el futuro se calcule.

A continuación se muestra un ejemplo sencillo, donde nos conformamos con programar una visualización. A continuación se muestra cómo se puede escribir un callback como este:

>>> def print_callback(future):  
...     print('Callback: future done: {}'.format(future.result()))  
... 

A continuación se muestra cómo utilizarlo:

>>> async def main():  
...     loop = asyncio.get_running_loop()  
...     future = loop.create_future()   
...     future.add_done_callback(print_callback)  
...     task = loop.create_task(compute_value(future))  
...     await future  
...     exception = task.exception()  
...     if exception:  
...         print(f"The subroutine raised an exception: 
{exception!r}") 
...     else:  
...         print(f"The subroutine returned: {future.result()}")  
...   
>>> asyncio.run(main())  
Callback: future done: 42  
The subroutine returned: 42 

La línea resaltada permite añadir este callback. Vemos que tiene lugar antes de la llamada concreta del futuro, lo que se hecho durante el uso de await.

Cada callback se puede añadir varias veces:

>>> async def main():  
...     loop = asyncio.get_running_loop()  
...     future = loop.create_future()  
...     future.add_done_callback(print_callback)  
...     future.add_done_callback(print_callback)   
...     future.add_done_callback(print_callback)  
...     task = loop.create_task(compute_value(future))  ...

La asincronía según las versiones de Python

1. Python 3.7

Python 3.7 introduce la función asyncio.run, que usamos mucho en este capítulo (y en el siguiente). Observe que esta funcionalidad se ha introducido a título probatorio. Por lo tanto, es posible que desaparezca o se modifique en futuras versiones. Por el momento, cuando se escribe este libro, parece que no se modificará para Python 3.8. Es de prever que esta función se confirme o sustituya para Python 3.9.

Retomemos un ejemplo del libro:

>>> async def main():  
...     loop = asyncio.get_running_loop()  
...     loop.call_soon(function, 42)  
...     loop.call_soon(partial(function, kwarg="other"), 34)  
...     loop.call_soon(partial(function, 16, kwarg="another"))  
...     await asyncio.sleep(1)  
...   
>>> asyncio.run(main()) 

Se puede sustituir por:

>>> async def main(loop):  
...     loop.call_soon(function, 42)  
...     loop.call_soon(partial(function, kwarg="other"), 34)  
...     loop.call_soon(partial(function, 16, kwarg="another"))  
...     await asyncio.sleep(1)  
...   
>>> event_loop = asyncio.get_event_loop()  
>>> event_loop.run_until_complete(main(event_loop))...

Caso concreto

1. Ejemplo de trabajo

Para este ejemplo, vamos a presentar un extracto de código no asíncrono, y después vamos a transformarlo en asíncrono. Este código hace dos acciones: descargar imágenes y guardarlas en un archivo. Estas dos acciones utilizan muchas operaciones de I/O, pero la primera se hace a través de la red y por lo tanto, sufre bastante tiempo de espera mientras que la segunda utiliza directamente el sistema de archivos y tiene poco de tiempo de espera, incluso si hay operaciones de I/O. Además, en la actualidad, el lenguaje Python no ofrece forma de escribir o leer archivos de manera asíncrona.

El primer módulo en el que podemos pensar para acceder a un recurso web, sería el excelente módulo requests.

Este último no es asíncrono y no ofrece trabajar de manera no bloqueante, como la documentación indica (https://2.python-requests.org/en/master/user/advanced/#blocking-or-non-blockingpython%20requests.org/en/master/user/advanced/#blocking-or-non-blocking). Por lo tanto, el programa se bloqueará desde el inicio de la descarga del recurso, hasta el final. Para trabajar en modo no bloqueante, la documentación ofrece tres alternativas: requests-thread que, como su nombre indica, ofrece una solución usando la programación en paralelo, grequests que, como su nombre indica más sutilmente, ofrece utilizar gevent, una forma de uso de hilos de ejecución ligeros y se ve como una alternativa a la asincronía (presentada en el capítulo Programación asíncrona: alternativas) y para terminar requests-future, que utiliza la noción de futuro, pero del módulo concurrent.future y no la noción de asyncio.Future, otra solución que implica la programación en paralelo.

Por lo tanto, veremos este caso de uso y sus alternativas en el capítulo sobre la programación en paralelo (Calidad), así como el de las alternativas a la asincronía (Generación de contenido).

Hecho este paréntesis, vamos...

Ejemplo trabajado de nuevo usando los generadores

¿Cuándo utilizar los generadores y cuando la asincronía? La asincronía se reserva para cuando se hacen acciones que implican trabajo externo (utilización de muchas operaciones de I/O que pausan el programa). Mientras que se trata de un algoritmo que manipula datos, si no queremos esperar a recolectar todos los datos antes de tratarlos, lo mejor es intentar utilizar los generadores.

Y los generadores no son exclusivos de la programación asíncrona: podemos utilizar los dos juntos. Vamos a detallar cómo transformar el código básico, para optimizarlo un poco:

El proceso de recuperación de las imágenes desde el código fuente HTML, se puede fácilmente transformar en generador:

def get_images_src_from_html(html_doc):    
    """Recupera todo el contenido de los atributos src de las etiquetas 
img"""   
    soup = BeautifulSoup(html_doc, "html.parser")    
    return (img.get('src') for img in soup.find_all('img')) 

Ha sido suficiente con sustituir los corchetes por paréntesis.

La siguiente etapa, que consiste en recuperar las URI absolutas, es más compleja. Por lo tanto, hay que utilizar esta técnica:

def get_uri_from_images_src(base_uri, images_src):    
    """Devuelve una a una cada URI de la imagen a descargar"""    
    parsed_base = urlparse(base_uri)    
    for src in images_src:    
        parsed = urlparse(src)    
        if parsed.netloc == '':    
            path = parsed.path    
            if parsed.query:    
                path += '?' + parsed.query    
            if path[0] != '/':    
                if parsed_base.path == '/':    
                    path...