Funções de modelo em Python que podem ser executadas de maneira síncrona e assíncrona

imagem



Agora, quase todo desenvolvedor está familiarizado com o conceito de "assincronia" na programação. Em uma era em que os produtos de informação são tão procurados que são forçados a processar simultaneamente um grande número de solicitações e também interagir em paralelo com um grande conjunto de outros serviços - sem programação assíncrona - em lugar nenhum. A necessidade acabou por ser tão grande que até foi criada uma linguagem separada, cuja principal característica (para além de ser minimalista) é um trabalho muito optimizado e conveniente com código paralelo / concorrente, nomeadamente Golang . Apesar do fato de que o artigo não é sobre ele, freqüentemente farei comparações e mencionarei. Mas aqui em Python, que será discutido neste artigo - há alguns problemas que irei descrever e oferecer uma solução para um deles. Se você estiver interessado neste tópico - por favor, em cat.






Acontece que minha linguagem favorita que uso para trabalhar, implementar projetos de estimação e até mesmo descansar e relaxar é o Python . Fico infinitamente cativado por sua beleza e simplicidade, sua obviedade, por trás da qual, com a ajuda de vários tipos de açúcar sintático, há enormes oportunidades para uma descrição lacônica de quase qualquer lógica de que a imaginação humana é capaz. Eu até li em algum lugar que Python é chamado de linguagem de nível ultra-alto, uma vez que pode ser usado para descrever abstrações que seriam extremamente problemáticas para descrever em outras linguagens.



Mas há uma nuance séria - Pythonmuito difícil de se encaixar nos conceitos modernos da linguagem com a possibilidade de implementação de lógica paralela / concorrente. A linguagem, cuja ideia se originou nos anos 80 e que tem a mesma idade do Java, até certa época não implicava a execução de nenhum código de forma competitiva. Se o JavaScript inicialmente exigia simultaneidade para trabalho sem bloqueio em um navegador, e Golang é uma linguagem completamente nova com uma compreensão real das necessidades modernas, então o Python nunca enfrentou tais tarefas antes.



Esta, é claro, é minha opinião pessoal, mas me parece que o Python está muito atrasado com a implementação da assincronia, desde o surgimento da biblioteca de assincronia embutidafoi, ao contrário, uma reação ao surgimento de outras implementações de execução simultânea de código para Python. Basicamente, asyncio é construído para suportar implementações existentes e contém não apenas sua própria implementação de loop de evento, mas também um wrapper para outras bibliotecas assíncronas, oferecendo assim uma interface comum para escrever código assíncrono. E o Python , que foi originalmente criado como a linguagem mais lacônica e legível devido a todos os fatores listados acima, ao escrever código assíncrono torna-se um monte de decoradores, geradores e funções. A situação foi ligeiramente corrigida pela adição de diretivas especiais async e await (como em JavaScript , que é importante) (corrigido, graças ao usuáriotmnhy), mas os problemas comuns permaneceram.



Não vou listá-los todos e vou me concentrar naquele que tentei resolver: esta é uma descrição da lógica geral para execução assíncrona e síncrona. Por exemplo, se eu quiser executar uma função em paralelo no Golang , então preciso apenas chamar a função com a diretiva go :



Execução paralela de função em 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")
}




Dito isso, em Golang, posso executar esta mesma função de forma síncrona:



Execução serial da função em 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")
}




Em Python, todas as co-rotinas (funções assíncronas) são baseadas em geradores e a troca entre eles ocorre durante a chamada de funções de bloqueio, retornando o controle para o loop de eventos usando a diretiva yield . Para ser honesto, não sei como a simultaneidade / simultaneidade funciona no Golang , mas não me engano se disser que não funciona da maneira que funciona no Python . Apesar das diferenças existentes nas partes internas da implementação do compilador Golang e do interpretador CPython e da inadmissibilidade de comparar paralelismo / simultaneidade nelas, ainda farei isso e prestarei atenção não à execução em si, mas à sintaxe. Em PythonNão posso pegar uma função e executá-la em paralelo / concorrentemente com um operador. Para que minha função funcione de forma assíncrona, devo escrevê-la explicitamente de forma assíncrona antes de sua declaração e , depois disso, ela não é mais apenas uma função, já é uma co-rotina. E não posso misturar suas chamadas em um código sem ações adicionais, porque uma função e uma co-rotina em Python são coisas completamente diferentes, apesar da semelhança na declaração.



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


Meu principal problema era a necessidade de desenvolver uma lógica que pudesse rodar tanto de forma síncrona quanto assíncrona. Um exemplo simples é minha biblioteca de interação com o Instagram , que abandonei há muito tempo, mas agora a retomei (o que me levou a buscar uma solução). Eu queria implementar nele a capacidade de trabalhar com a API não apenas de forma síncrona, mas também de forma assíncrona, e isso não era apenas um desejo - ao coletar dados na Internet, você pode enviar um grande número de solicitações de forma assíncrona e obter uma resposta para todas elas mais rápido, mas ao mesmo tempo a coleta massiva de dados não é sempre necessário. No momento, a biblioteca implementa o seguinte: para trabalhar com o InstagramExistem 2 classes, uma para trabalho síncrono e outra para assíncrono. Cada classe possui o mesmo conjunto de métodos, apenas na primeira os métodos são síncronos e, na segunda, são assíncronos. Cada método faz a mesma coisa - exceto como as solicitações são enviadas para a Internet. E apenas por causa das diferenças em uma ação de bloqueio, tive que duplicar quase completamente a lógica em cada método. Se parece com isso:



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)
        ...


Tudo o mais no método de atualização e na co - rotina de atualização é absolutamente idêntico. E como muitas pessoas sabem, a duplicação de código adiciona muitos problemas, especialmente quando se trata de corrigir bugs e testes.



Eu escrevi minha própria biblioteca pySyncAsync para resolver esse problema . A ideia é a seguinte - em vez de funções e corrotinas comuns, um gerador é implementado, no futuro vou chamá-lo de modelo. Para executar um modelo, ele precisa ser gerado como uma função regular ou como uma co-rotina. O template, quando executado no momento em que precisa executar código assíncrono ou síncrono dentro de si, retorna um objeto Call especial usando yield, que especifica o que invocar e com quais argumentos. Dependendo de como o template será gerado - como uma função ou como uma co-rotina - desta forma os métodos descritos no objeto Call serão executados .



Vou mostrar um pequeno exemplo de um modelo que assume a capacidade de fazer solicitações ao google :



Exemplo de solicitações do 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"))




Vou falar um pouco sobre a estrutura interna da biblioteca. Existe uma classe Manager , na qual funções e corrotinas são registradas para serem chamadas usando Call . Também é possível registrar modelos, mas isso é opcional. A classe Manager possui métodos de registro , geração e modelo . Os mesmos métodos do exemplo acima foram chamados diretamente do pysyncasync , mas usaram uma instância global da classe Manager , que já foi criada em um dos módulos da biblioteca. Na verdade, você pode criar sua própria instância e chamar os métodos register , generate e template a partir dela.isolando assim os gerentes uns dos outros se, por exemplo, um conflito de nomes for possível.



O método de registro atua como um decorador e permite que você registre uma função ou co-rotina para chamada posterior do modelo. O decorador de registro aceita como argumento o nome sob o qual a função ou co-rotina é registrada no gerenciador. Se o nome não for especificado, a função ou co-rotina é registrada com seu próprio nome.



O método do modelo permite que você registre o gerador como um modelo no gerenciador. Isso é necessário para obter um modelo por nome.



Método de geraçãopermite gerar uma função ou co-rotina com base em um modelo. Leva dois argumentos: o primeiro é o nome do modelo ou o próprio modelo, o segundo é "sincronizar" ou "async" - como gerar o modelo - para uma função ou para uma co-rotina. Na saída, o método generate fornece uma função pronta ou co-rotina.



Vou dar um exemplo de geração de um modelo, por exemplo, em uma co-rotina:



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


Dentro, uma co-rotina é gerada, que simplesmente itera sobre o gerador e recebe objetos da classe Call , em seguida, pega a co-rotina registrada anteriormente pelo nome (o nome é obtido da chamada ), a chama com argumentos (que também recebe da chamada ) e o resultado da execução dessa co-rotina também armazena na chamada .



Os objetos da classe Call são simplesmente contêineres para armazenar informações sobre o que e como chamar e também permitem que você armazene o resultado em si. wrapper também pode retornar o resultado da execução do template; para isso, o template é embalado em uma classe Generator especial , que não é mostrada aqui.



Omiti algumas das nuances, mas espero ter transmitido a essência em geral.



Para ser honesto, este artigo foi escrito por mim em vez de compartilhar minhas idéias sobre como resolver problemas com código assíncrono em Python.e, o mais importante, ouvir as opiniões dos residentes de Khabrav. Talvez eu coloque alguém em outra solução, talvez alguém discorde dessa implementação em particular e diga como você pode torná-la melhor, talvez alguém lhe diga por que tal solução não é necessária e você não deve misturar síncrona e código assíncrono, a opinião de cada um de vocês é muito importante para mim. Além disso, não pretendo ser verdadeiro em todo o meu raciocínio no início do artigo. Pensei muito no tópico de outros YPs e posso estar enganado, além disso, há a possibilidade de que eu possa confundir os conceitos, por favor, se você notar alguma inconsistência - descreva nos comentários. Também ficarei feliz se houver alterações na sintaxe e na pontuação.



E obrigado por sua atenção a este assunto e a este artigo em particular!



All Articles