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 *