Funciones de plantilla en Python que se pueden ejecutar de forma sincr贸nica y asincr贸nica

imagen



Ahora casi todos los desarrolladores est谩n familiarizados con el concepto de "asincron铆a" en la programaci贸n. En una era en la que los productos de informaci贸n tienen tanta demanda que se ven obligados a procesar simult谩neamente una gran cantidad de solicitudes y tambi茅n a interactuar en paralelo con un gran conjunto de otros servicios, sin programaci贸n asincr贸nica, en ninguna parte. La necesidad result贸 ser tan grande que incluso se cre贸 un lenguaje separado, cuya caracter铆stica principal (adem谩s de ser minimalista) es un trabajo muy optimizado y conveniente con c贸digo paralelo / concurrente, a saber, Golang . A pesar de que el art铆culo no trata sobre 茅l en absoluto, a menudo har茅 comparaciones y me referir茅 a 茅l. Pero aqu铆 en Python, que se discutir谩 en este art铆culo, hay algunos problemas que describir茅 y ofrecer茅 una soluci贸n a uno de ellos. Si est谩 interesado en este tema, por favor, debajo de cat.






Dio la casualidad de que mi lenguaje favorito, con el que trabajo, implemento proyectos favoritos e incluso descanso y me relajo, es Python . Me cautiva infinitamente su belleza y simplicidad, su obviedad, detr谩s de la cual, con la ayuda de varios tipos de az煤car sint谩ctico, hay enormes oportunidades para una descripci贸n lac贸nica de casi cualquier l贸gica de la que sea capaz la imaginaci贸n humana. Incluso le铆 en alguna parte que Python se llama lenguaje de nivel ultra alto, ya que se puede usar para describir abstracciones que ser铆an extremadamente problem谩ticas de describir en otros lenguajes.



Pero hay un matiz serio: Pythonmuy dif铆cil de encajar en conceptos modernos del lenguaje con la posibilidad de implementar l贸gica paralela / concurrente. El lenguaje, cuya idea se origin贸 en los a帽os 80 y que tiene la misma edad que Java, hasta cierto momento no implic贸 la ejecuci贸n de ning煤n c贸digo de forma competitiva. Si JavaScript inicialmente requer铆a concurrencia para el trabajo sin bloqueo en el navegador, y Golang es un lenguaje completamente nuevo con una comprensi贸n real de las necesidades modernas, entonces Python no ten铆a tales tareas antes.



Esta, por supuesto, es mi opini贸n personal, pero me parece que Python est谩 muy retrasado con la implementaci贸n de la asincron铆a, desde la aparici贸n de la librer铆a asyncio incorporadam谩s bien, fue una reacci贸n a la aparici贸n de otras implementaciones de ejecuci贸n de c贸digo concurrente para Python. B谩sicamente, asyncio est谩 construido para admitir implementaciones existentes y contiene no solo su propia implementaci贸n de bucle de eventos, sino tambi茅n un contenedor para otras bibliotecas asincr贸nicas, ofreciendo as铆 una interfaz com煤n para escribir c贸digo asincr贸nico. Y Python , que se cre贸 originalmente como el lenguaje m谩s lac贸nico y legible debido a todos los factores enumerados anteriormente, cuando la escritura de c贸digo asincr贸nico se convierte en un revoltijo de decoradores, generadores y funciones. La situaci贸n se corrigi贸 ligeramente mediante la adici贸n de directivas especiales async y await (como en JavaScript , lo cual es importante) (corregido, gracias al usuariotmnhy), pero persistieron problemas comunes.



No los enumerar茅 todos y me centrar茅 en uno que intent茅 resolver: esta es una descripci贸n de la l贸gica general para la ejecuci贸n as铆ncrona y s铆ncrona. Por ejemplo, si quiero ejecutar una funci贸n en paralelo en Golang , solo necesito llamar a la funci贸n con la directiva go :



Ejecuci贸n paralela de funciones en Golang
package main

import "fmt"

func function(index int) {
    fmt.Println("function", index)
}

func main() {
    for i := 0; i < 10; i++ { 
        go function(i)
    }
    fmt.Println("end")
}




Dicho esto, en Golang, puedo ejecutar esta misma funci贸n sincr贸nicamente:



Ejecuci贸n serial de funci贸n en Golang
package main

import "fmt"

func function(index int) {
    fmt.Println("function", index)
}

func main() {
    for i := 0; i < 10; i++ { 
        function(i)
    }
    fmt.Println("end")
}




En Python, todas las corrutinas (funciones asincr贸nicas) se basan en generadores y el cambio entre ellos ocurre durante la llamada de funciones de bloqueo, devolviendo el control al bucle de eventos mediante la directiva yield . Para ser honesto, no s茅 c贸mo funciona la concurrencia / concurrencia en Golang , pero no me equivoco si digo que no funciona como lo hace en Python . A pesar de las diferencias existentes en los aspectos internos de la implementaci贸n del compilador Golang y el int茅rprete CPython y la inadmisibilidad de comparar paralelismo / concurrencia en ellos, seguir茅 haciendo esto y prestar茅 atenci贸n no a la ejecuci贸n en s铆, sino a la sintaxis. En PythonNo puedo tomar una funci贸n y ejecutarla en paralelo / al mismo tiempo con un operador. Para que mi funci贸n funcione de forma asincr贸nica, debo escribirla expl铆citamente de forma asincr贸nica antes de su declaraci贸n, y despu茅s de eso ya no es solo una funci贸n, ya es una corrutina. Y no puedo mezclar sus llamadas en el mismo c贸digo sin acciones adicionales, porque una funci贸n y una corrutina en Python son cosas completamente diferentes, a pesar de la similitud en la declaraci贸n.



def func1(a, b):
    func2(a + b)
    await func3(a - b)  # ,   await     


Mi principal problema fue la necesidad de desarrollar una l贸gica que se pueda ejecutar tanto de forma sincr贸nica como asincr贸nica. Un ejemplo simple es mi biblioteca para la interacci贸n con Instagram , que abandon茅 hace mucho tiempo, pero ahora la retom茅 (lo que me impuls贸 a buscar una soluci贸n). Quer铆a implementar en 茅l la capacidad de trabajar con la API no solo de forma s铆ncrona, sino tambi茅n asincr贸nica, y esto no era solo un deseo: al recopilar datos en Internet, puede enviar una gran cantidad de solicitudes de forma asincr贸nica y obtener una respuesta a todas ellas m谩s r谩pido, pero al mismo tiempo, la recopilaci贸n masiva de datos no es siempre necesitado. Por el momento, la biblioteca implementa lo siguiente: para trabajar con Instagramhay 2 clases, una para trabajo s铆ncrono y otra para as铆ncrono. Cada clase tiene el mismo conjunto de m茅todos, solo que en el primero los m茅todos son s铆ncronos y en el segundo son asincr贸nicos. Cada m茅todo hace lo mismo, excepto por c贸mo se env铆an las solicitudes a Internet. Y solo debido a las diferencias en una acci贸n de bloqueo, tuve que duplicar casi por completo la l贸gica en cada m茅todo. Se parece a esto:



class WebAgent:
    def update(self, obj=None, settings=None):
        ...
        response = self.get_request(path=path, **settings)
        ...

class AsyncWebAgent:
    async def update(self, obj=None, settings=None):
        ...
        response = await self.get_request(path=path, **settings)
        ...


Todo lo dem谩s en el m茅todo de actualizaci贸n y en la rutina de actualizaci贸n es absolutamente id茅ntico. Y como mucha gente sabe, la duplicaci贸n de c贸digo agrega muchos problemas, especialmente cuando se trata de corregir errores y realizar pruebas.



Escrib铆 mi propia biblioteca pySyncAsync para resolver este problema . La idea es la siguiente: en lugar de funciones ordinarias y corrutinas, se implementa un generador, en el futuro lo llamar茅 plantilla. Para ejecutar una plantilla, debe generarla como una funci贸n regular o como una corrutina. La plantilla, cuando se ejecuta en el momento en que necesita ejecutar c贸digo asincr贸nico o s铆ncrono dentro de s铆 misma, devuelve un objeto Call especial usando yield, que especifica a qu茅 llamar y con qu茅 argumentos. Dependiendo de c贸mo se generar谩 la plantilla, como funci贸n o como corrutina, as铆 es como se ejecutar谩n los m茅todos descritos en el objeto Call .



Mostrar茅 un peque帽o ejemplo de una plantilla que asume la capacidad de realizar solicitudes a Google :



Ejemplo de solicitudes de google usando pySyncAsync
import aiohttp
import requests

import pysyncasync as psa

#       google
#          Call
@psa.register("google_request")
def sync_google_request(query, start):
    response = requests.get(
        url="https://google.com/search",
        params={"q": query, "start": start},
    )
    return response.status_code, dict(response.headers), response.text


#       google
#          Call
@psa.register("google_request")
async def async_google_request(query, start):
    params = {"q": query, "start": start}
    async with aiohttps.ClientSession() as session:
        async with session.get(url="https://google.com/search", params=params) as response:
            return response.status, dict(response.headers), await response.text()


#     100 
def google_search(query):
    start = 0
    while start < 100:
        #  Call     ,        google_request
        call = Call("google_request", query, start=start)
        yield call
        status, headers, text = call.result
        print(status)
        start += 10


if __name__ == "__main__":
    #   
    sync_google_search = psa.generate(google_search, psa.SYNC)
    sync_google_search("Python sync")

    #   
    async_google_search = psa.generate(google_search, psa.ASYNC)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(async_google_search("Python async"))




Te contar茅 un poco sobre la estructura interna de la biblioteca. Existe una clase Manager en la que se registran funciones y corrutinas para ser llamadas usando Call . Tambi茅n es posible registrar plantillas, pero esto es opcional. La clase Manager tiene m茅todos de registro , generaci贸n y plantilla . Los mismos m茅todos en el ejemplo anterior fueron llamados directamente desde pysyncasync , solo que usaron una instancia global de la clase Manager , que ya fue creada en uno de los m贸dulos de la biblioteca. De hecho, puede crear su propia instancia y llamar a los m茅todos de registro , generaci贸n y plantilla a partir de ella.aislando as铆 a los gerentes unos de otros si, por ejemplo, es posible un conflicto de nombres.



El m茅todo de registro act煤a como decorador y le permite registrar una funci贸n o corrutina para una llamada posterior desde la plantilla. El decorador de registros acepta como argumento el nombre bajo el cual se registra la funci贸n o corrutina en el administrador. Si no se especifica el nombre, la funci贸n o corrutina se registra con su propio nombre.



El m茅todo de plantilla le permite registrar el generador como una plantilla en el administrador. Esto es necesario para poder obtener una plantilla por su nombre. Generar



m茅todole permite generar una funci贸n o corrutina basada en una plantilla. Se necesitan dos argumentos: el primero es el nombre de la plantilla o la plantilla en s铆, el segundo es "sincronizar" o "as铆ncrono" - qu茅 generar la plantilla - para una funci贸n o para una corrutina. En la salida, el m茅todo generate proporciona una funci贸n o corrutina lista para usar.



Dar茅 un ejemplo de c贸mo generar una plantilla, por ejemplo, en una corrutina:



def _async_generate(self, template):
    async def wrapper(*args, **kwargs):
        ...
        for call in template(*args, **kwargs):
            callback = self._callbacks.get(f"{call.name}:{ASYNC}")
            call.result = await callback(*call.args, **call.kwargs)
        ...
    return wrapper


En su interior se genera una corrutina, que simplemente itera sobre el generador y recibe objetos de la clase Call , luego toma la corrutina previamente registrada por su nombre (el nombre se toma de la llamada ), la llama con argumentos (que tambi茅n toma de la llamada ) y el resultado de ejecutar esta corrutina tambi茅n se almacena en la llamada .



Los objetos de la clase Call son simplemente contenedores para almacenar informaci贸n sobre qu茅 y c贸mo llamar y tambi茅n le permiten almacenar el resultado en s铆 mismos. wrapper tambi茅n puede devolver el resultado de la ejecuci贸n de la plantilla; para esto, la plantilla est谩 envuelta en una clase Generator especial , que no se muestra aqu铆.



He omitido algunos de los matices, pero espero haber transmitido la esencia en general.



Para ser honesto, este art铆culo fue escrito por m铆 en lugar de compartir mis pensamientos sobre c贸mo resolver problemas con c贸digo asincr贸nico en Python.y, lo m谩s importante, escuchar las opiniones de los residentes de Khabrav. Tal vez encuentre a alguien con otra soluci贸n, tal vez alguien no est茅 de acuerdo con esta implementaci贸n en particular y le diga c贸mo puede mejorarla, tal vez alguien le diga por qu茅 tal soluci贸n no es necesaria en absoluto y no debe mezclar s铆ncrona y c贸digo asincr贸nico, la opini贸n de cada uno de ustedes es muy importante para m铆. Adem谩s, no pretendo ser cierto en todo mi razonamiento al comienzo del art铆culo. Pens茅 mucho en el tema de otros idiomas y podr铆a estar equivocado, adem谩s, existe la posibilidad de que pueda confundir los conceptos, por favor, si de repente nota alguna inconsistencia, descr铆bala en los comentarios. Tambi茅n me alegrar谩 si hay modificaciones en la sintaxis y la puntuaci贸n.



隆Y gracias por su atenci贸n a este tema y a este art铆culo en particular!



All Articles