Tutorial de Python async/await

La programación asíncrona ha ganado mucha fuerza en los últimos años, y por una buena razón. Aunque puede resultar más difícil que el tradicional...

La programación asíncrona ha ganado mucha fuerza en los últimos años, y por una buena razón. Aunque puede ser más difícil que el estilo lineal tradicional, también es mucho más eficiente.

Por ejemplo, en lugar de esperar a que finalice una solicitud HTTP antes de continuar con la ejecución, con las corrutinas asíncronas de Python puede enviar la solicitud y realizar otro trabajo que está esperando en una cola mientras espera que finalice la solicitud HTTP. Es posible que necesite pensar un poco más para obtener la lógica correcta, pero podrá manejar mucho más trabajo con menos recursos.

Incluso entonces, la sintaxis y la ejecución de funciones asincrónicas en lenguajes como Python en realidad no son tan difíciles. Ahora, JavaScript es una historia diferente, pero Python parece ejecutarlo bastante bien.

La asincronía parece ser una gran razón por la cual Node.js es tan popular para la programación del lado del servidor. Gran parte del código que escribimos, especialmente en aplicaciones de IO pesadas como sitios web, depende de recursos externos. Esto podría ser cualquier cosa, desde una llamada a una base de datos remota hasta la publicación de un servicio REST. Tan pronto como solicita cualquiera de estos recursos, su código está esperando sin nada que hacer.

Con la programación asincrónica, permite que su código maneje otras tareas mientras espera que estos otros recursos respondan.

Corrutinas

Una función asíncrona en Python normalmente se llama 'coroutine', que es solo una función que usa la palabra clave async, o una que está decorada con @asyncio.coroutine. Cualquiera de las siguientes funciones funcionaría como una rutina y son efectivamente equivalentes en tipo:

1
2
3
4
5
6
7
8
import asyncio

async def ping_server(ip):
    pass

@asyncio.coroutine
def load_file(path):
    pass

Estas son funciones especiales que devuelven objetos coroutine cuando se les llama. Si está familiarizado con las Promesas de JavaScript, entonces puede pensar en este objeto devuelto casi como una Promesa. Llamar a cualquiera de estos en realidad no los ejecuta, sino que se devuelve un objeto rutina, que luego se puede pasar al bucle de eventos que se ejecutará más adelante.

En caso de que necesite determinar si una función es una rutina o no, asyncio proporciona el método [asyncio.iscoroutinefunction(función)](https://docs.python.org/3/library/asyncio-task.html #asyncio.iscoroutinefunction) que hace exactamente esto por usted. O bien, si necesita determinar si un objeto devuelto por una función es un objeto coroutine, puede usar [asincio.iscoroutine(obj)](https://docs.python.org/3/library/asyncio-task .html#asyncio.iscoroutine) en su lugar.

Rendimiento de

Hay algunas formas de llamar a una corrutina, una de las cuales es el método rendimiento de. Esto se introdujo en Python 3.3 y se mejoró aún más en Python 3.5 en forma de async/await (que veremos más adelante).

La expresión rendimiento de se puede utilizar de la siguiente manera:

1
2
3
4
5
import asyncio

@asyncio.coroutine
def get_json(client, url):
    file_content = yield from load_file('/Users/scott/data.txt')

Como puede ver, rendimiento de se usa dentro de una función decorada con @asyncio.coroutine. Si intentara usar yield from fuera de esta función, obtendría un error de Python como este:

1
2
3
4
  File "main.py", line 1
    file_content = yield from load_file('/Users/scott/data.txt')
                  ^
SyntaxError: 'yield' outside function

Para usar esta sintaxis, debe estar dentro de otra función (típicamente con el decorador de rutina).

Asíncrono/espera

La sintaxis más nueva y limpia es usar las palabras clave async/await. Introducido en Python 3.5, async se usa para declarar una función como coroutine, muy parecido a lo que hace el decorador @asyncio.coroutine. Se puede aplicar a la función colocándolo al principio de la definición:

1
2
async def ping_server(ip):
    # ping code here...

Para llamar a esta función, usamos await, en lugar de yield from, pero de la misma manera:

1
2
async def ping_local():
    return await ping_server('192.168.1.1')

Nuevamente, al igual que rendimiento de, no puede usar esto fuera de otra rutina, de lo contrario obtendrá un error de sintaxis.

En Python 3.5, se admiten ambas formas de llamar a coroutines, pero la forma async/await está destinada a ser la sintaxis principal.

Ejecutar el ciclo de eventos {#ejecutar el ciclo de eventos}

Ninguna de las rutinas que describí anteriormente importará (o funcionará) si no sabe cómo iniciar y ejecutar un [bucle de eventos] (https://docs.python.org/3/library/asyncio-eventloop .html). El bucle de eventos es el punto central de ejecución de las funciones asincrónicas, por lo que cuando desee ejecutar la rutina, esto es lo que utilizará.

El bucle de eventos le proporciona bastantes características:

  • Registrar, ejecutar y cancelar llamadas retrasadas (funciones asíncronas)
  • Crear transportes de cliente y servidor para la comunicación.
  • Crear subprocesos y transportes para la comunicación con otro programa
  • Llamadas de funciones delegadas a un grupo de subprocesos

Si bien en realidad hay bastantes configuraciones y tipos de bucles de eventos que puede usar, la mayoría de los programas que escriba solo necesitarán usar algo como esto para programar una función:

1
2
3
4
5
6
7
8
import asyncio

async def speak_async():
    print('OMG asynchronicity!')

loop = asyncio.get_event_loop()
loop.run_until_complete(speak_async())
loop.close()

Las últimas tres líneas son lo que nos interesa aquí. Comienza obteniendo el bucle de eventos predeterminado (asyncio.get_event_loop()), programando y ejecutando la tarea asíncrona, y luego cerrando el bucle cuando termina de ejecutarse.

La función loop.run_until_complete() en realidad está bloqueando, por lo que no regresará hasta que todos los métodos asincrónicos hayan terminado. Dado que solo estamos ejecutando esto en un solo hilo, no hay forma de que pueda avanzar mientras el ciclo está en progreso.

Ahora, puede pensar que esto no es muy útil ya que terminamos bloqueando el ciclo de eventos de todos modos (en lugar de solo las llamadas IO), pero imagine envolver todo su programa en una función asíncrona, que luego le permitiría ejecutar muchos solicitudes asíncronas al mismo tiempo, como en un servidor web.

Incluso podría dividir el bucle de eventos en su propio subproceso, permitiéndole manejar todas las solicitudes de E/S largas mientras el subproceso principal maneja la lógica del programa o la interfaz de usuario.

Un ejemplo

Bien, veamos un ejemplo un poco más grande que podamos ejecutar. El siguiente código es un programa asíncrono bastante simple que obtiene JSON de Reddit, analiza el JSON e imprime las principales publicaciones del día de /r/python, /r/programming y /r/compsci.

El primer método que se muestra, get_json(), es llamado por get_reddit_top() y simplemente crea una solicitud HTTP GET a la URL de Reddit apropiada. Cuando se llama a esto con await, el bucle de eventos puede continuar y dar servicio a otras corrutinas mientras espera que regrese la respuesta HTTP. Una vez que lo hace, el JSON se devuelve a get_reddit_top(), se analiza y se imprime.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import signal
import sys
import asyncio
import aiohttp
import json

loop = asyncio.get_event_loop()
client = aiohttp.ClientSession(loop=loop)

async def get_json(client, url):
    async with client.get(url) as response:
        assert response.status == 200
        return await response.read()

async def get_reddit_top(subreddit, client):
    data1 = await get_json(client, 'https://www.reddit.com/r/' + subreddit + '/top.json?sort=top&t=day&limit=5')

    j = json.loads(data1.decode('utf-8'))
    for i in j['data']['children']:
        score = i['data']['score']
        title = i['data']['title']
        link = i['data']['url']
        print(str(score) + ': ' + title + ' (' + link + ')')

    print('DONE:', subreddit + '\n')

def signal_handler(signal, frame):
    loop.stop()
    client.close()
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)

asyncio.ensure_future(get_reddit_top('python', client))
asyncio.ensure_future(get_reddit_top('programming', client))
asyncio.ensure_future(get_reddit_top('compsci', client))
loop.run_forever()

Esto es un poco diferente al código de muestra que mostramos anteriormente. Para que varias corrutinas se ejecuten en el ciclo de eventos, usamos asyncio.ensure_future() y luego ejecutamos el ciclo para siempre para procesar todo.

Para ejecutar esto, primero deberá instalar aiohttp, lo que puede hacer con PIP:

1
$ pip install aiohttp

Ahora solo asegúrese de ejecutarlo con Python 3.5 o superior, y debería obtener un resultado como este:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ python main.py
46: Python async/await Tutorial (https://wikihtp.com/python-async-await-tutorial/)
16: Using game theory (and Python) to explain the dilemma of exchanging gifts. Turns out: giving a gift probably feels better than receiving one... (http://vknight.org/unpeudemath/code/2015/12/15/The-Prisoners-Dilemma-of-Christmas-Gifts/)
56: Which version of Python do you use? (This is a poll to compare the popularity of Python 2 vs. Python 3) (http://strawpoll.me/6299023)
DONE: python

71: The Semantics of Version Control - Wouter Swierstra (http://www.staff.science.uu.nl/~swier004/Talks/vc-semantics-15.pdf)
25: Favorite non-textbook CS books (https://www.reddit.com/r/compsci/comments/3xag9e/favorite_nontextbook_cs_books/)
13: CompSci Weekend SuperThread (December 18, 2015) (https://www.reddit.com/r/compsci/comments/3xacch/compsci_weekend_superthread_december_18_2015/)
DONE: compsci

1752: 684.8 TB of data is up for grabs due to publicly exposed MongoDB databases (https://blog.shodan.io/its-still-the-data-stupid/)
773: Instagram's Million Dollar Bug? (http://exfiltrated.com/research-Instagram-RCE.php)
387: Amazingly simple explanation of Diffie-Hellman. His channel has tons of amazing videos and only a few views :( thought I would share! (https://www.youtube.com/watch?v=Afyqwc96M1Y)
DONE: programming

Tenga en cuenta que si ejecuta esto varias veces, el orden en que se imprimen los datos de subreddit cambia. Esto se debe a que cada una de las llamadas que hacemos libera (cede) el control del subproceso, lo que permite que se procese otra llamada HTTP. Cualquiera que regrese primero se imprime primero.

Conclusión

Aunque la funcionalidad asincrónica incorporada de Python no es tan fluida como la de JavaScript, eso no significa que no pueda usarla para aplicaciones interesantes y eficientes. Solo tómese 30 minutos para aprender sus entresijos y tendrá una mejor idea de cómo puede integrar esto en sus propias aplicaciones.

¿Qué opinas del async/await de Python? ¿Cómo lo has usado en el pasado? ¡Cuéntanos en los comentarios!