Python: hacer un retraso de tiempo (suspensión) para la ejecución de código

En esta guía, veremos cómo retrasar la ejecución del código/hacer un retraso de tiempo/dormir en Python, tanto para código sincrónico como asincrónico con ejemplos.

Introducción

Retraso de código (también conocido como dormir) es exactamente lo que su nombre implica, el retraso de la ejecución del código durante un período de tiempo. La necesidad más común de retraso del código es cuando estamos esperando que finalice otro proceso, para que podamos trabajar con el resultado de ese proceso. En los sistemas de subprocesos múltiples, un subproceso puede querer esperar a que otro subproceso finalice una operación para continuar trabajando con ese resultado.

Otro ejemplo podría ser disminuir la tensión en un servidor con el que estamos trabajando. Por ejemplo, durante el web scraping (éticamente) y siguiendo los términos del servicio del sitio web en cuestión, respetando el archivo robots.txt, es posible que desee retrasar la ejecución de cada solicitud para no abrumar los recursos de la servidor.

Muchas solicitudes, disparadas en rápida sucesión, pueden, dependiendo del servidor en cuestión, tomar rápidamente todas las conexiones libres y convertirse efectivamente en un DoS Attack. Para permitir un respiro, así como para asegurarnos de no afectar negativamente a los usuarios del sitio web o al sitio web en sí, limitaríamos la cantidad de solicitudes enviadas retrasando cada una.

Un estudiante, esperando los resultados del examen, puede actualizar furiosamente el sitio web de su escuela, esperando noticias. Alternativamente, pueden escribir un script que verifique si el sitio web tiene algo nuevo. En cierto sentido, retraso de código puede convertirse técnicamente en programación de código con un ciclo válido y una condición de finalización, suponiendo que el mecanismo de retraso en su lugar no sea bloqueo.

En este artículo, veremos cómo retrasar la ejecución del código en Python, también conocido como dormir.

Código de retraso con time.sleep()

Una de las soluciones más comunes al problema es la función sleep() del módulo integrado time. Acepta la cantidad de segundos que le gustaría que dure el proceso, a diferencia de muchos otros idiomas que se basan en milisegundos:

1
2
3
4
5
6
import datetime
import time

print(datetime.datetime.now().time())
time.sleep(5)
print(datetime.datetime.now().time())

Esto resulta en:

1
2
14:33:55.282626
14:34:00.287661

Claramente, podemos ver un retraso de 5 segundos entre las dos sentencias print(), con una precisión bastante alta, hasta el segundo decimal. Si desea dormir por menos de * 1 * segundo, también puede pasar fácilmente números no enteros:

1
2
3
print(datetime.datetime.now().time())
time.sleep(0.25)
print(datetime.datetime.now().time())
1
2
14:46:16.198404
14:46:16.448840
1
2
3
print(datetime.datetime.now().time())
time.sleep(1.28)
print(datetime.datetime.now().time())
1
2
14:46:16.448911
14:46:17.730291

Sin embargo, tenga en cuenta que con 2 decimales, la duración del sueño podría no ser exactamente en el lugar, especialmente porque es difícil de probar, dado que las instrucciones print() toman algún tiempo (variable) para ejecutar también.

Sin embargo, hay una gran desventaja en la función time.sleep(), muy notable en entornos de subprocesos múltiples.

time.sleep() está bloqueando.

Se apodera del hilo en el que está y lo bloquea durante el sueño. Esto lo hace inadecuado para tiempos de espera más largos, ya que obstruye el hilo del procesador durante ese período de tiempo. Además, esto lo hace inadecuado para Aplicaciones asíncronas y reactivas, que a menudo requieren datos y comentarios en tiempo real.

Otra cosa a tener en cuenta sobre time.sleep() es el hecho de que no puedes detenerlo. Una vez que comienza, no puede cancelarlo externamente sin terminar todo el programa o si hace que el método sleep() arroje una excepción, lo que lo detendría.

Programación asíncrona y reactiva {#programación asíncrona y reactiva}

Programación asíncrona gira en torno a la ejecución en paralelo, donde una tarea se puede ejecutar y finalizar independientemente del flujo principal.

En la programación síncrona, si una Función A llama a la Función B, detiene la ejecución hasta que la Función B finaliza, después de lo cual se puede reanudar la Función A.

En la programación asincrónica, si una Función A llama a la Función B, independientemente de su dependencia del resultado de la Función B, ambas pueden ejecutarse al mismo tiempo y, si es necesario, esperar a que la otra lo haga. terminar de utilizar los demás resultados.

Programación reactiva es un subconjunto de Programación asíncrona, que desencadena la ejecución de código reactivamente, cuando se presentan datos, independientemente de si la función que se supone que los procesa ya está ocupada . La programación reactiva depende en gran medida de las arquitecturas basadas en mensajes (donde un mensaje suele ser un evento o un comando).

Tanto las aplicaciones asincrónicas como las reactivas son las que sufren mucho con el bloqueo de código, por lo que usar algo como time.sleep() no es una buena opción para ellas. Echemos un vistazo a algunas opciones de retraso de código sin bloqueo.

Código de retraso con asyncio.sleep()

Asyncio es una biblioteca de Python dedicada a escribir código concurrente y usa la sintaxis async/await, que puede ser familiar para los desarrolladores que la han usado en otros lenguajes.

Instalemos el módulo a través de pip:

1
$ pip install asyncio

Una vez instalado, podemos importarlo a nuestro script y reescribir nuestra función:

1
2
3
4
5
6
7
import asyncio
async def main():
    print(datetime.datetime.now().time())
    await asyncio.sleep(5)
    print(datetime.datetime.now().time())

asyncio.run(main())

Cuando trabajamos con asyncio, marcamos las funciones que se ejecutan de forma asíncrona como async y esperamos los resultados de operaciones como asyncio.sleep() que finalizarán en algún momento en el futuro.

Similar al ejemplo anterior, esto imprimirá dos veces, con 5 segundos de diferencia:

1
2
17:23:33.708372
17:23:38.716501

Sin embargo, esto realmente no ilustra la ventaja de usar asyncio.sleep(). Reescribamos el ejemplo para ejecutar algunas tareas en paralelo, donde esta distinción es mucho más clara:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import asyncio
import datetime

async def intense_task(id):
    await asyncio.sleep(5)
    print(id, 'Running some labor-intensive task at ', datetime.datetime.now().time())

async def main():
    await asyncio.gather(
        asyncio.create_task(intense_task(1)),
        asyncio.create_task(intense_task(2)),
        asyncio.create_task(intense_task(3))
    )

asyncio.run(main())

Aquí, tenemos una función async, que simula una tarea que requiere mucha mano de obra y tarda 5 segundos en finalizar. Luego, usando asyncio, creamos múltiples tareas. Sin embargo, cada tarea puede ejecutarse de forma asíncrona, solo si las llamamos de forma asíncrona. Si tuviéramos que ejecutarlos secuencialmente, también se ejecutarían secuencialmente.

Para llamarlas en paralelo, usamos la función gather(), que, bueno, reúne las tareas y las ejecuta:

1
2
3
1 Running some labor-intensive task at  17:35:21.068469
2 Running some labor-intensive task at  17:35:21.068469
3 Running some labor-intensive task at  17:35:21.068469

Estos son todos ejecutados al mismo tiempo, y el tiempo de espera para los tres no es de 15 segundos - son 5.

Por otro lado, si modificamos este código para usar time.sleep() en su lugar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import asyncio
import datetime
import time

async def intense_task(id):
    time.sleep(5)
    print(id, 'Running some labor-intensive task at ', datetime.datetime.now().time())

async def main():
    await asyncio.gather(
        asyncio.create_task(intense_task(1)),
        asyncio.create_task(intense_task(2)),
        asyncio.create_task(intense_task(3))
    )

asyncio.run(main())

Estaríamos esperando 5 segundos entre cada instrucción print():

1
2
3
1 Running some labor-intensive task at  17:39:00.766275
2 Running some labor-intensive task at  17:39:05.773471
3 Running some labor-intensive task at  17:39:10.784743

Código de retraso con temporizador

La clase Timer es un Thread, que puede ejecutar y ejecutar operaciones solo después de que haya pasado un cierto período de tiempo. Este comportamiento es exactamente lo que estamos buscando, sin embargo, es un poco exagerado usar Threads para retrasar el código si aún no está trabajando con un sistema de subprocesos múltiples.

La clase Timer necesita start(), y puede detenerse mediante cancel(). Su constructor acepta un número entero, que indica la cantidad de segundos que se debe esperar antes de ejecutar el segundo parámetro: una función.

Hagamos una función y ejecútela a través de un Temporizador:

1
2
3
4
5
6
7
8
9
from threading import Timer
import datetime

def f():
    print("Code to be executed after a delay at:", datetime.datetime.now().time())

print("Code to be executed immediately at:", datetime.datetime.now().time())
timer = Timer(3, f)
timer.start()

Esto resulta en:

1
2
Code to be executed immediately at: 19:47:20.032525
Code to be executed after a delay at: 19:47:23.036206

El método cancel() es realmente útil si tenemos varias funciones ejecutándose y nos gustaría cancelar la ejecución de una función, en función de los resultados de otra o de otra condición.

Escribamos una función f(), que llama tanto a f2() como a f3(). f2() se llama tal cual, y devuelve un número entero aleatorio entre 1 y 10, simulando el tiempo que llevó ejecutar esa función.

f3() se llama a través de un Timer y si el resultado de f2() es mayor que 5, f3() se cancela, mientras que si f2() se ejecuta en el "esperado " tiempo de menos de 5 - f3() se ejecuta después de que finaliza el temporizador:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from threading import Timer
import datetime
import random

def f():
    print("Executing f1 at", datetime.datetime.now().time())
    result = f2()
    timer = Timer(5, f3)
    timer.start()
    if(result > 5):
        print("Cancelling f3 since f2 resulted in", result)
        timer.cancel()

def f2():
    print("Executing f2 at", datetime.datetime.now().time())
    return random.randint(1, 10)

def f3():
    print("Executing f3 at", datetime.datetime.now().time())

f()

Ejecutar este código varias veces se vería algo así como:

1
2
3
4
5
6
7
Executing f1 at 20:29:10.709578
Executing f2 at 20:29:10.709578
Cancelling f3 since f2 resulted in 9

Executing f1 at 20:29:14.178362
Executing f2 at 20:29:14.178362
Executing f3 at 20:29:19.182505

Código de retraso con evento

La clase Event se puede utilizar para generar eventos. Un solo evento puede ser "escuchado" por varios subprocesos. La función Event.wait() bloquea el subproceso en el que está, a menos que Event.isSet(). Una vez que set() un evento, todos los subprocesos que esperaron se activan y el Event.wait() se convierte en sin bloqueo.

Esto se puede usar para sincronizar subprocesos: todos se acumulan y esperan () hasta que se establece un determinado evento, después de lo cual, pueden dictar su flujo.

Vamos a crear un método waiter y ejecutarlo varias veces en diferentes subprocesos. Cada mesero comienza a trabajar a una hora determinada y verifica cada segundo si todavía está a la hora, justo antes de tomar un pedido, que tarda un segundo en completarse. Estarán trabajando hasta que se establezca el Evento, o mejor dicho, su tiempo de trabajo haya terminado.

Cada camarero tendrá su propio hilo, mientras que la gestión reside en el hilo principal, y llamará cuando todos puedan llamar a casa. Como hoy se sienten muy generosos, reducirán el tiempo de trabajo y dejarán que los camareros se vayan a casa después de 4 segundos de trabajo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import threading
import time
import datetime

def waiter(event, id):
    print(id, "Waiter started working at", datetime.datetime.now().time())
    event_flag = end_of_work.wait(1)
    while not end_of_work.isSet():
        print(id, "Waiter is taking order at", datetime.datetime.now().time())
        event.wait(1)
    if event_flag:
        print(id, "Waiter is going home at",  datetime.datetime.now().time())

end_of_work = threading.Event()

for id in range(1, 3):
    thread = threading.Thread(target=waiter, args=(end_of_work, id))
    thread.start()

end_of_work.wait(4)
end_of_work.set()
print("Some time passes, management was nice and cut the working hours short. It is now", datetime.datetime.now().time())

Ejecutar este código da como resultado:

1
2
3
4
5
6
7
8
9
1 Waiter started working at 23:20:34.294844
2 Waiter started working at 23:20:34.295844
1 Waiter is taking order at 23:20:35.307072
2 Waiter is taking order at 23:20:35.307072
1 Waiter is taking order at 23:20:36.320314
2 Waiter is taking order at 23:20:36.320314
1 Waiter is taking order at 23:20:37.327528
2 Waiter is taking order at 23:20:37.327528
Some time passes, management was nice and cut the working hours short. It is now 23:20:38.310763

El evento end_of_work se usó aquí para sincronizar los dos subprocesos y controlar cuándo funcionan y cuándo no, retrasando la ejecución del código por un tiempo establecido entre las comprobaciones.

Conclusión

En esta guía, hemos analizado varias formas de retrasar la ejecución de código en Python, cada una aplicable a un contexto y requisito diferente.

El método normal time.sleep() es bastante útil para la mayoría de las aplicaciones, aunque no es realmente óptimo para largos tiempos de espera, no se usa comúnmente para programación simple y está bloqueando.

Usando asyncio, tenemos una versión asíncrona de time.sleep() que podemos esperar.

La clase Timer retrasa la ejecución del código y puede cancelarse si es necesario.

La clase Event genera eventos que varios subprocesos pueden escuchar y responder en consecuencia, lo que retrasa la ejecución del código hasta que se establece un determinado evento.

Licensed under CC BY-NC-SA 4.0