Corutinas en Python

    Introducci贸n

    Cada programador est谩 familiarizado con las funciones: secuencias de instrucciones agrupadas como una sola unidad para realizar tareas predeterminadas. Admiten un 煤nico punto de entrada, son capaces de aceptar argumentos, pueden tener o no un valor de retorno, y pueden ser llamados en cualquier momento durante la ejecuci贸n de un programa, incluso por otras funciones y por ellos mismos.

    Cuando un programa llama a una funci贸n, su contexto de ejecuci贸n actual se guarda antes de pasar el control a la funci贸n y reanudar la ejecuci贸n. A continuaci贸n, la funci贸n crea un nuevo contexto; a partir de ah铆, los datos reci茅n creados existen exclusivamente durante el tiempo de ejecuci贸n de las funciones.

    Tan pronto como se completa la tarea, el control se transfiere de nuevo a la persona que llama: el nuevo contexto se elimina efectivamente y se reemplaza por el anterior.

    Corutinas

    Las corrutinas son un tipo especial de funci贸n que deliberadamente cede el control a la persona que llama, pero no finaliza su contexto en el proceso, sino que lo mantiene en un estado inactivo.

    Se benefician de la capacidad de conservar sus datos durante toda su vida y, a diferencia de las funciones, pueden tener varios puntos de entrada para suspender y reanudar la ejecuci贸n.

    Las corrutinas en Python funcionan de manera muy similar a los generadores. Ambos operan sobre datos, as铆 que mantengamos las principales diferencias simples:

    Generadores Produce datos

    Corutinas consumir datos

    El manejo distinto de la palabra clave yield determina si estamos manipulando uno u otro.

    Definici贸n de una corrutina

    Con todos los elementos esenciales fuera del camino, saltemos y codifiquemos nuestra primera corrutina:

    def bare_bones():
        while True:
            value = (yield)
    

    Es evidente el parecido con una funci贸n Python normal. los while True: block garantiza la ejecuci贸n continua de la corrutina mientras reciba valores.

    El valor se recoge a trav茅s del yield declaraci贸n. Volveremos a esto en unos momentos …

    Est谩 claro que este c贸digo es pr谩cticamente in煤til, as铆 que lo redondearemos con algunos print declaraciones:

    def bare_bones():
        print("My first Coroutine!")
        while True:
            value = (yield)
            print(value)
    

    Ahora, 驴qu茅 sucede cuando intentamos llamarlo as铆?

    coroutine = bare_bones()
    

    Si esta fuera una funci贸n normal de Python, uno esperar铆a que produjera alg煤n tipo de salida en este punto. Pero si ejecuta el c贸digo en su estado actual, notar谩 que ni un solo print() se llama.

    Esto se debe a que las corrutinas requieren next() m茅todo que se llamar谩 primero:

    def bare_bones():
        print("My first Coroutine!")
        while True:
            value = (yield)
            print(value)
    
    coroutine = bare_bones()
    next(coroutine)
    

    Esto inicia la ejecuci贸n de la corrutina hasta que alcanza su primer punto de interrupci贸n – value = (yield). Luego, se detiene, regresa la ejecuci贸n al main y permanece inactivo mientras espera una nueva entrada:

    My first Coroutine!
    

    Se puede enviar una nueva entrada con send():

    coroutine.send("First Value")
    

    Nuestra variable value entonces recibir谩 la cadena First Value, impr铆malo y una nueva iteraci贸n del while True: loop obliga a la corrutina a esperar una vez m谩s a que se entreguen nuevos valores. Puedes hacer esto tantas veces como quieras.

    Finalmente, una vez que haya terminado con la corrutina y ya no desee utilizarla, puede liberar esos recursos llamando close(). Esto plantea un GeneratorExit excepci贸n que debe tratarse:

    def bare_bones():
        print("My first Coroutine!")
        try:
            while True:
                value = (yield)
                print(value)
        except GeneratorExit:
            print("Exiting coroutine...")
    
    coroutine = bare_bones()
    next(coroutine)
    coroutine.send("First Value")
    coroutine.send("Second Value")
    coroutine.close()
    

    Salida:

    My first Coroutine!
    First Value
    Second Value
    Exiting coroutine...
    

    Pasar argumentos

    Al igual que las funciones, las corrutinas tambi茅n son capaces de recibir argumentos:

    def filter_line(num):
        while True:
            line = (yield)
            if num in line:
                print(line)
    
    cor = filter_line("33")
    next(cor)
    cor.send("Jessica, age:24")
    cor.send("Marco, age:33")
    cor.send("Filipe, age:55")
    

    Salida:

    Marco, age:33
    

    Aplicar varios puntos de interrupci贸n

    M煤ltiple yield Las declaraciones se pueden secuenciar juntas en la misma corrutina individual:

    def joint_print():
        while True:
            part_1 = (yield)
            part_2 = (yield)
            print("{} {}".format(part_1, part_2))
    
    cor = joint_print()
    next(cor)
    cor.send("So Far")
    cor.send("So Good")
    

    Salida:

    So Far So Good
    

    La excepci贸n StopIteration

    Despu茅s de cerrar una corrutina, llamar send() nuevamente generar谩 un StopIteration excepci贸n:

    def test():
        while True:
            value = (yield)
            print(value)
    try:
        cor = test()
        next(cor)
        cor.close()
        cor.send("So Good")
    except StopIteration:
        print("Done with the basics")
    

    Salida:

    Done with the basics
    

    Corutinas con decoradores

    隆Todo esto est谩 muy bien! Pero cuando se trabaja en proyectos m谩s grandes, se inicia cada uno 隆Coroutine manualmente puede ser una gran lata!

    No se preocupe, es solo cuesti贸n de explotar el poder de los Decoradores, por lo que ya no necesitamos usar el next() m茅todo:

    def coroutine(func):
        def start(*args, **kwargs):
            cr = func(*args, **kwargs)
            next(cr)
            return cr
        return start
    
    @coroutine
    def bare_bones():
        while True:
            value = (yield)
            print(value)
    
    cor = bare_bones()
    cor.send("Using a decorator!")
    

    Ejecutar este fragmento de c贸digo producir谩:

    Using a decorator!
    

    Construcci贸n de tuber铆as

    Una canalizaci贸n es una secuencia de elementos de procesamiento organizados de modo que la salida de cada elemento sea la entrada del siguiente.

    Los datos pasan por la tuber铆a hasta que finalmente se consumen. Cada canalizaci贸n requiere al menos una fuente y uno lavabo.

    Las etapas restantes de la tuber铆a pueden realizar varias operaciones diferentes, desde filtrar hasta modificar, enrutar y reducir datos:

    Las corrutinas son candidatos naturales para realizar estas operaciones, pueden pasar datos entre s铆 con send() operaciones y tambi茅n puede servir como consumidor final. Veamos el siguiente ejemplo:

    def producer(cor):
        n = 1
        while n < 100:
            cor.send(n)
            n = n * 2
    
    @coroutine
    def my_filter(num, cor):
        while True:
            n = (yield)
            if n < num:
                cor.send(n)
    
    @coroutine
    def printer():
        while True:
            n = (yield)
            print(n)
    
    prnt = printer()
    filt = my_filter(50, prnt)
    producer(filt)
    

    Salida:

    1
    2
    4
    8
    16
    32
    

    Entonces, lo que tenemos aqu铆 es el producer() actuando como fuente, creando unos valores que luego se filtran antes de ser impresos por el sumidero, en este caso, el printer() corrutina.

    my_filter(50, prnt) act煤a como el 煤nico paso intermedio en la tuber铆a y recibe su propia corrutina como argumento.

    Este encadenamiento ilustra perfectamente la fuerza de las corrutinas: son escalables para proyectos m谩s grandes (todo lo que se requiere es agregar m谩s etapas a la tuber铆a) y f谩ciles de mantener (los cambios en una no fuerzan una reescritura completa del c贸digo fuente).

    Similitudes con los objetos

    Un programador perspicaz podr铆a darse cuenta de que las corrutinas contienen cierta similitud conceptual con los objetos de Python. Desde la definici贸n previa requerida hasta la declaraci贸n y gesti贸n de instancias. Surge la pregunta obvia de por qu茅 uno usar铆a corrutinas sobre el paradigma probado y verdadero de la programaci贸n orientada a objetos.

    Bueno, aparte del hecho obvio de que las corrutinas solo requieren una definici贸n de funci贸n 煤nica, tambi茅n se benefician de ser significativamente m谩s r谩pidas. Examinemos el siguiente c贸digo:

    class obj:
        def __init__(self, value):
            self.i = value
        def send(self, num):
            print(self.i + num)
    
    inst = obj(1)
    inst.send(5)
    
    def coroutine(value):
        i = value
        while True:
            num = (yield)
            print(i + num)
    
    cor = coroutine(1)
    next(cor)
    cor.send(5)
    

    As铆 es como estos dos se enfrentan entre s铆, cuando atravesaron el timeit m贸dulo, 10,000 veces:

    Corutina de objetos

    0,7918110,6343617
    0,79970580,6383156
    0.85792860,6365501
    0.8384390,648442
    0,96042550,7242559

    Ambos realizan la misma tarea servil, pero el segundo ejemplo es m谩s r谩pido. La velocidad gana el advenimiento de la ausencia del objeto self b煤squedas.

    Para tareas m谩s exigentes para el sistema, esta caracter铆stica constituye una raz贸n de peso para usar corrutinas en lugar de los objetos de manejo convencionales.

    Precauci贸n al usar corrutinas

    El m茅todo send () no es seguro para subprocesos

    import threading
    from time import sleep
    
    def print_number(cor):
        while True:
            cor.send(1)
    
    def coroutine():
        i = 1
        while True:
            num = (yield)
            print(i)
            sleep(3)
            i += num
    
    cor = coroutine()
    next(cor)
    
    t = threading.Thread(target=print_number, args=(cor,))
    t.start()
    
    while True:
        cor.send(5)
    

    Porque send() no se sincroniz贸 correctamente, ni tiene protecci贸n inherente contra llamadas incorrectas relacionadas con subprocesos, se gener贸 el siguiente error: ValueError: generator already executing.

    La combinaci贸n de corrutinas con simultaneidad debe realizarse con extrema precauci贸n.

    No es posible realizar un bucle de corrutinas

    def coroutine_1(value):
        while True:
            next_cor = (yield)
            print(value)
            value = value - 1
            if next_cor != None:
                next_cor.send(value)
    
    def coroutine_2(next_cor):
        while True:
            value = (yield)
            print(value)
            value = value - 2
            if next != None:
                next_cor.send(value)
    
    cor1 = coroutine_1(20)
    next(cor1)
    cor2 = coroutine_2(cor1)
    next(cor2)
    cor1.send(cor2)
    

    Lo mismo ValueError muestra su rostro. De estos simples ejemplos podemos inferir que el send() El m茅todo crea una especie de pila de llamadas que no regresa hasta que el objetivo alcanza su yield declaraci贸n.

    Por lo tanto, el uso de corrutinas no es solo luz del sol y arco iris, se debe pensar cuidadosamente antes de la aplicaci贸n.

    Conclusi贸n

    Las corrutinas proporcionan una poderosa alternativa a los mecanismos habituales de procesamiento de datos. Las unidades de c贸digo se pueden combinar, modificar y reescribir f谩cilmente, al mismo tiempo que se benefician de la persistencia variable a lo largo de su ciclo de vida.

    En manos de un programador astuto, las corrutinas se convierten en nuevas herramientas significativas al permitir un dise帽o y una implementaci贸n m谩s simples, al mismo tiempo que proporcionan importantes ganancias de rendimiento.

    Reducir las ideas a procesos sencillos ahorra tiempo y esfuerzo al programador, al tiempo que evita rellenar el c贸digo con objetos superfluos que no hacen m谩s que tareas elementales.

    .

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *