Red de conocimiento informático - Material del sitio web - ¿Usando el sistema Asyncio en Python (3-4)?

¿Usando el sistema Asyncio en Python (3-4)?

Tareas y futuros

Anteriormente analizamos las corrutinas y su utilidad cuando se ejecutan en un bucle. Ahora quiero hablar brevemente sobre la API Task y Future. La que usará más es Task, ya que la mayor parte de su trabajo implicará ejecutar la rutina usando la función create_task(), como se configura en "Inicio rápido" en la página 22. La clase Future es en realidad una superclase de Task, que proporciona toda la funcionalidad para interactuar con bucles.

Se puede entender simplemente: el futuro representa el estado de finalización futura de una actividad y se gestiona mediante un bucle. La tarea es exactamente la misma, pero la "actividad" específica es una rutina, probablemente una que usted creó usando la función async def más create_task().

La clase Future representa el estado de algo que interactúa con un bucle. Esta descripción es demasiado vaga para ser útil, por lo que puede considerar la instancia futura como un conmutador, un conmutador para el estado de finalización. Cuando se crea la instancia futura, el interruptor se establece en el estado "aún no completado", pero luego pasará al estado "completado". De hecho, las instancias futuras tienen un método llamado done() que le permite verificar el estado, como se muestra en el Ejemplo 3-15.

Ejemplo 3-15. Utilice el método done() para verificar el estado de finalización

Las instancias futuras también pueden realizar las siguientes operaciones:

Establecer un valor de resultado ( usando . set_result(value) establece el valor y usa .result() para obtener el valor)

¿Usa el método .cancel() para cancelar (y usará .cancelled() para verificar si es así? cancelado)

? Agregar una función que vuelve a llamar cuando se completa Future

Incluso si Task es más común, no es posible evitar por completo el uso de Future: por ejemplo, ejecutar una función. en un ejecutor devolverá una instancia futura, no una tarea. Echemos un vistazo rápido al Ejemplo 3-16 para ver cómo es trabajar directamente con una instancia Future.

Ejemplo 3-16. Interacción con instancias futuras

(L3) Cree una función principal simple. Ejecutamos la función, esperamos un momento y luego establecemos un resultado en Future f.

(L5) Establecer un resultado.

(L8) Crear manualmente una instancia Futura. Tenga en cuenta que esta instancia está (de forma predeterminada) vinculada a nuestro bucle, pero no está ni estará adjunta a ninguna rutina (para eso están las Tareas).

(L9) Antes de hacer nada, confirma que el futuro aún no está completado.

(L11) Organizar la corrutina main() y pasar el futuro. Recuerde, todo lo que hace la corrutina main() es dormir y luego cambiar instancias futuras. (Tenga en cuenta que la corrutina main() no comenzará a ejecutarse todavía: la corrutina solo comienza a ejecutarse cuando el bucle de eventos se está ejecutando).

(L13) Aquí usamos run_until_complete en la instancia Future en lugar de la instancia Task (). Esto no se parece a nada que hayas visto antes. Ahora que el ciclo se está ejecutando, la rutina main() comenzará a ejecutarse.

(L16) Finalmente, cuando se establezca el resultado del futuro, estará completo. Una vez completado, se puede acceder a los resultados.

Por supuesto, es poco probable que utilices Future directamente de la forma que se muestra aquí; los ejemplos de código son solo para fines educativos. La mayor parte de su contacto con asynccio se realiza a través de instancias de tareas.

Quizás te preguntes qué sucede si se llama a set_result() en una instancia de Task. Esto era posible antes de Python 3.8, pero ya no está permitido. Las instancias de tareas son envoltorios alrededor de objetos de rutina y sus valores de resultado solo se pueden establecer internamente en los resultados de la función de rutina subyacente, como se muestra en el Ejemplo 3-17.

Ejemplo 3-17. Llamar a set_result en una tarea

(L13) La única diferencia es que creamos una instancia de Tarea en lugar de una instancia de Futuro. Por supuesto, la API de tareas requiere que proporcionemos una rutina; aquí usamos sleep() solo porque es simple y conveniente;

(L7) está pasando una instancia de Tarea. Satisface la firma de tipo de la función (porque Task es una subclase de Future), pero a partir de Python 3.8, ya no podemos llamar a set_result() en una Task: intentar hacerlo generará un RuntimeError. La idea es que una tarea represente una rutina en ejecución, por lo que los resultados siempre deben provenir de la tarea misma.

(L10, L24) Sin embargo, aún podemos cancelar() una tarea y generará un CancelledError en la rutina subyacente.

¿Crear_tarea? Garantizar_futuro?

En el "Inicio rápido" en la página 22, dije que la forma de ejecutar una corrutina es usar asyncio.create_task(). Antes de introducir esta función, es necesario obtener una instancia de bucle y utilizar loop.create_task() para realizar la misma tarea. De hecho, esto también se puede lograr mediante una función diferente a nivel de módulo: asyncio.ensure_future(). Algunos desarrolladores recomiendan create_task(), mientras que otros recomiendan sure_future().

Durante mi investigación para este libro, me convencí de que el método API asyncio.ensure_future() era el culpable de malentendidos generalizados sobre la biblioteca asyncio. La mayor parte de la API es bastante clara, pero existen algunas barreras importantes para el aprendizaje, y esta es una de ellas. Cuando encuentres sure_future(), tu cerebro intentará con todas sus fuerzas integrarlo en un modelo mental de cómo se debe usar asyncio, ¡pero probablemente fallará!

En la documentación de asyncio de Python 3.6, ¡esta ahora infame explicación! resalta el problema con asegurar_future():

asyncio.ensure_future(coro_or_future, *, _loop =None)

Programar la ejecución de un objeto de rutina: está envuelto en el futuro. Devuelve un objeto Tarea. Si el parámetro es un futuro, se devuelve directamente.

¿¡Qué!? Cuando leí este artículo por primera vez, estaba confundido. Se espera que la siguiente sea una descripción más clara de sure_future():

Esta función es una buena ilustración de la API asyncio (API de alto nivel) para desarrolladores de usuarios finales y la API asyncio (API de bajo nivel) para diseñadores de marcos).

Repasemos el ejemplo 3-18 para ver cómo funciona.

Ejemplo 3-18. Una mirada más cercana a lo que está haciendo sure_future()

(L3) Una función de rutina simple que no hace nada. Solo necesitamos algo para formar la corrutina.

(L6) Creamos el objeto de rutina llamando directamente a esta función. Su código rara vez hará esto, pero quiero dejar claro aquí que estamos pasando un objeto de rutina a cada create_task() y sure_future().

(L7) Consigue un bucle.

(L9) Primero, usamos loop.create_task() para programar la corrutina en el bucle y devolver una nueva instancia de Task.

(L10) Tipo de verificación. Hasta ahora nada interesante.

(L12) Mostramos que asyncio.ensure_future() se puede usar para realizar las mismas acciones que create_task(): pasamos una rutina y devolvimos una instancia de Tarea (y la rutina está programada para ejecutarse). en un bucle)! Si se pasa una rutina, no hay diferencia entre loop.create_task() y asyncio.ensure_future().

(L15) ¿Qué sucede si pasamos una instancia de Task a asegurar_future()? Tenga en cuenta que la instancia de Task que queremos pasar se creó a través de loop.create_task() en el paso 4.

(L16) La instancia de Task devuelta es exactamente la misma que la instancia de Task pasada: no se modificó cuando se pasó.

¿Cuál es el punto de pasar instancias futuras directamente? ¿Por qué utilizar la misma función para hacer dos cosas diferentes? La respuesta es que el propósito de sure_future() es que los autores del marco proporcionen a los desarrolladores de usuarios finales una API que pueda manejar ambos parámetros. ¿No me crees? Esto es lo que dijo el propio ex-BDFL:

El objetivo de asegurar_future() es que si tienes algo que podría ser una corrutina o un Futuro (este último incluye una Tarea ya que es una subclase de Futuro), y desea poder llamar a un método solo definido en un futuro (probablemente el único ejemplo útil sea cancelar()). Cuando ya es un Futuro (o Tarea), no hace nada; cuando es una corrutina, la envuelve en una Tarea.

Si sabe que tiene una corrutina y desea programarla, la API correcta es create_task(). El único momento en el que se debe llamar a sure_future() es cuando proporcionas una API (como la mayoría de las API de asyncio) que acepta una corrutina o un Futuro y necesitas hacer algo con ella que requiere que tengas un Futuro.

—Guido van Rossum

En resumen, asyncio.sure_future() es una función auxiliar para los diseñadores de frameworks. Esto se explica más fácilmente por analogía con una función más común, así que hagámoslo. Si tiene algunos años de experiencia en programación, es posible que haya visto funciones similares a la función istify() en el Ejemplo 3-19. La función listify() en el ejemplo 3-19.

Ejemplo 3-19. Una función de utilidad que fuerza una lista como entrada

Esta función intenta convertir el argumento en una lista, independientemente de la entrada.

Este tipo de función se usa a menudo en API y marcos para convertir la entrada a un tipo conocido, lo que simplificará el código posterior; en este caso, sabrá que el parámetro (la salida de listify()) siempre será una lista.

Si cambio el nombre de la función listify() a sure_list(), entonces deberías empezar a ver similitudes con asyncio.ensure_future(): siempre intenta convertir el argumento a un tipo Futuro (o subclase). Esta es una función de utilidad que facilita la vida a los desarrolladores de marcos (a diferencia de los desarrolladores de usuarios finales como usted y yo).

De hecho, el módulo de biblioteca estándar asyncio usa asegurar_future() por este motivo. La próxima vez que mire la API, encontrará que los parámetros de la función se describen como "objetos en espera", muy probablemente usando asegura_future() internamente para convertir los parámetros. Por ejemplo, la función asyncio.gather() se parece al siguiente código:

Los parámetros de aws representan "objetos en espera", incluidas corrutinas, tareas y futuros. Internamente, together() usa sure_future() para la coerción de tipo: la tarea y el futuro permanecen sin cambios, y la corrutina se convierte en una tarea.

El punto clave aquí es que, como desarrollador de aplicaciones de usuario final, nunca debería necesitar utilizar asyncio.ensure_future(). Es más una herramienta de diseñador de marcos. Si necesita programar una rutina en el bucle de eventos, simplemente use asyncio.create_task() para hacerlo directamente.

En las próximas secciones, volveremos a las funciones a nivel de idioma, comenzando con los administradores de contexto asincrónicos.