Vorlagenfunktionen in Python, die synchron und asynchron ausgeführt werden können

Bild



Mittlerweile ist fast jeder Entwickler mit dem Konzept der "Asynchronität" in der Programmierung vertraut. In einer Zeit, in der Informationsprodukte so gefragt sind, dass sie gezwungen sind, gleichzeitig eine große Anzahl von Anfragen zu verarbeiten und parallel zu einer Vielzahl anderer Dienste zu interagieren - ohne asynchrone Programmierung - nirgendwo. Das Bedürfnis stellte sich als so groß heraus, dass sogar eine separate Sprache erstellt wurde, deren Hauptmerkmal (zusätzlich zu ihrer Minimalität) eine sehr optimierte und bequeme Arbeit mit parallelem / gleichzeitigem Code ist, nämlich Golang . Trotz der Tatsache, dass der Artikel überhaupt nicht über ihn handelt, werde ich oft Vergleiche anstellen und darauf verweisen. Aber hier in Python, die in diesem Artikel behandelt werden - es gibt einige Probleme, die ich beschreiben und eine Lösung für eines von ihnen anbieten werde. Wenn Sie sich für dieses Thema interessieren - bitte unter Katze.






Zufällig ist Python meine Lieblingssprache, mit der ich arbeite, Haustierprojekte umsetze und mich sogar ausruhe und entspanne . Ich bin unendlich fasziniert von seiner Schönheit und Einfachheit, seiner Offensichtlichkeit, hinter der sich mit Hilfe verschiedener Arten von syntaktischem Zucker enorme Möglichkeiten für eine lakonische Beschreibung fast jeder Logik befinden, zu der die menschliche Vorstellungskraft fähig ist. Ich habe sogar irgendwo gelesen, dass Python eine Sprache auf höchstem Niveau genannt wird, da es verwendet werden kann, um Abstraktionen zu beschreiben, deren Beschreibung in anderen Sprachen äußerst problematisch wäre.



Aber es gibt eine ernsthafte Nuance - PythonEs ist sehr schwer, sich in moderne Sprachkonzepte einzufügen, mit der Möglichkeit, parallele / gleichzeitige Logik zu implementieren. Die Sprache, deren Idee in den 80er Jahren entstand und die bis zu einer bestimmten Zeit im gleichen Alter wie Java war, bedeutete nicht, dass Code wettbewerbsfähig ausgeführt wurde. Wenn JavaScript ursprünglich Parallelität für die nicht blockierende Arbeit in einem Browser erforderte und Golang eine völlig neue Sprache mit einem echten Verständnis der modernen Anforderungen ist, hat Python solche Aufgaben bisher noch nicht bewältigt .



Dies ist natürlich meine persönliche Meinung, aber es scheint mir, dass Python mit der Implementierung der Asynchronität sehr spät ist, da die eingebaute Asyncio- Bibliothek erscheintwar vielmehr eine Reaktion auf die Entstehung anderer Implementierungen der gleichzeitigen Codeausführung für Python. Grundsätzlich unterstützt asyncio vorhandene Implementierungen und enthält nicht nur eine eigene Ereignisschleifenimplementierung, sondern auch einen Wrapper für andere asynchrone Bibliotheken, sodass eine gemeinsame Schnittstelle zum Schreiben von asynchronem Code bereitgestellt wird. Und Python , das ursprünglich aufgrund aller oben aufgeführten Faktoren als lakonischste und lesbarste Sprache entwickelt wurde, wird beim Schreiben von asynchronem Code zu einem Durcheinander von Dekoratoren, Generatoren und Funktionen. Die Situation wurde leicht korrigiert, indem spezielle Anweisungen asynchron hinzugefügt und abgewartet wurden (wie in JavaScript , was wichtig ist) (dank des Benutzers korrigiert )tmnhy), aber gemeinsame Probleme blieben.



Ich werde sie nicht alle auflisten und mich auf die konzentrieren, die ich zu lösen versucht habe: Dies ist eine Beschreibung der allgemeinen Logik für die asynchrone und synchrone Ausführung. Wenn ich beispielsweise eine Funktion in Golang parallel ausführen möchte , muss ich die Funktion nur mit der Anweisung go aufrufen :



Parallele Funktionsausführung in 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")
}




Davon abgesehen kann ich in Golang dieselbe Funktion synchron ausführen:



Serienausführung der Funktion in 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")
}




In Python basieren alle Coroutinen (asynchrone Funktionen) auf Generatoren, und der Wechsel zwischen ihnen erfolgt während des Aufrufs blockierender Funktionen, wodurch die Steuerung mithilfe der Yield- Direktive an die Ereignisschleife zurückgegeben wird . Um ehrlich zu sein, weiß ich nicht, wie Parallelität / Parallelität in Golang funktioniert , aber ich irre mich nicht, wenn ich sage, dass es nicht so funktioniert wie in Python . Trotz der bestehenden Unterschiede in den Interna der Implementierung des Golang- Compilers und des CPython- Interpreters und der Unzulässigkeit, Parallelität / Parallelität in ihnen zu vergleichen, werde ich dies weiterhin tun und nicht auf die Ausführung selbst, sondern auf die Syntax achten. In PythonIch kann eine Funktion nicht übernehmen und sie parallel / gleichzeitig mit einem Operator ausführen. Damit meine Funktion asynchron funktioniert, muss ich sie vor ihrer Deklaration explizit asynchron schreiben. Danach ist sie nicht mehr nur eine Funktion, sondern bereits eine Coroutine. Und ich kann ihre Aufrufe nicht ohne zusätzliche Aktionen in einem Code mischen, da eine Funktion und eine Coroutine in Python trotz der Ähnlichkeit in der Deklaration völlig unterschiedliche Dinge sind.



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


Mein Hauptproblem war die Notwendigkeit, eine Logik zu entwickeln, die sowohl synchron als auch asynchron ausgeführt werden kann. Ein einfaches Beispiel ist meine Bibliothek für die Interaktion mit Instagram , die ich vor langer Zeit aufgegeben habe, jetzt aber wieder aufgenommen habe (was mich dazu veranlasste, nach einer Lösung zu suchen). Ich wollte darin die Fähigkeit implementieren, nicht nur synchron, sondern auch asynchron mit der API zu arbeiten, und dies war nicht nur ein Wunsch - wenn Sie Daten im Internet sammeln, können Sie eine große Anzahl von Anfragen asynchron senden und eine Antwort auf alle schneller erhalten, gleichzeitig jedoch keine massive Datenerfassung immer gebraucht. Im Moment implementiert die Bibliothek Folgendes: für die Arbeit mit InstagramEs gibt zwei Klassen, eine für synchrones Arbeiten und eine für asynchrones Arbeiten. Jede Klasse hat den gleichen Satz von Methoden, nur in der ersten sind die Methoden synchron und in der zweiten sind sie asynchron. Jede Methode macht das Gleiche - außer wie Anfragen an das Internet gesendet werden. Und nur wegen der Unterschiede in einer Blockierungsaktion musste ich die Logik in jeder Methode fast vollständig duplizieren. Es sieht aus wie das:



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


Alles andere in der Update- Methode und in der Update- Coroutine ist absolut identisch. Und wie viele wissen, bringt die Codeduplizierung viele Probleme mit sich, insbesondere wenn es darum geht, Fehler zu beheben und zu testen.



Ich habe meine eigene pySyncAsync- Bibliothek geschrieben, um dieses Problem zu lösen . Die Idee ist wie folgt: Anstelle von gewöhnlichen Funktionen und Coroutinen wird ein Generator implementiert, in Zukunft werde ich ihn eine Vorlage nennen. Um eine Vorlage auszuführen, müssen Sie sie als reguläre Funktion oder als Coroutine generieren. Wenn die Vorlage zu dem Zeitpunkt ausgeführt wird, zu dem sie asynchronen oder synchronen Code in sich selbst ausführen muss, gibt sie ein spezielles Call- Objekt mit Yield zurück, die angibt, was und mit welchen Argumenten aufgerufen werden soll. Abhängig davon, wie die Vorlage generiert wird - als Funktion oder als Coroutine - werden auf diese Weise die im Call- Objekt beschriebenen Methoden ausgeführt .



Ich werde ein kleines Beispiel einer Vorlage zeigen, die die Fähigkeit voraussetzt, Anfragen an Google zu stellen :



Beispiel für Google-Anfragen mit 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"))




Ich erzähle Ihnen ein wenig über die interne Struktur der Bibliothek. Es gibt eine Manager- Klasse , in der Funktionen und Coroutinen registriert sind, um mit Call aufgerufen zu werden . Es ist auch möglich, Vorlagen zu registrieren, dies ist jedoch optional. Die Manager- Klasse verfügt über Methoden zum Registrieren , Generieren und Vorlegen von Methoden . Die gleichen Methoden im obigen Beispiel wurden direkt von pysyncasync aufgerufen , nur dass sie eine globale Instanz der Manager- Klasse verwendeten , die bereits in einem der Bibliotheksmodule erstellt wurde. Tatsächlich können Sie Ihre eigene Instanz erstellen und die Register- , Generierungs- und Vorlagenmethoden daraus aufrufen.So werden Manager voneinander isoliert, wenn beispielsweise ein Namenskonflikt möglich ist.



Die Registrierungsmethode fungiert als Dekorator und ermöglicht es Ihnen, eine Funktion oder Coroutine für den weiteren Aufruf aus der Vorlage zu registrieren. Der Registerdekorateur akzeptiert als Argument den Namen, unter dem die Funktion oder Coroutine im Manager registriert ist. Wenn der Name nicht angegeben wird, wird die Funktion oder Coroutine unter ihrem eigenen Namen registriert.



Mit der Vorlagenmethode können Sie den Generator als Vorlage im Manager registrieren. Dies ist erforderlich, um eine Vorlage mit Namen zu erhalten.



Methode generierenErmöglicht das Generieren einer Funktion oder Coroutine basierend auf einer Vorlage. Es werden zwei Argumente benötigt: Das erste ist der Name der Vorlage oder der Vorlage selbst, das zweite ist "Sync" oder "Async" - wie die Vorlage generiert wird - für eine Funktion oder eine Coroutine. Am Ausgang gibt die Erzeugungsmethode eine vorgefertigte Funktion oder Coroutine aus.



Ich werde ein Beispiel für das Generieren einer Vorlage geben, zum Beispiel in einer Coroutine:



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


Im Inneren wird eine Coroutine generiert, die einfach über den Generator iteriert und Objekte der Call- Klasse empfängt , dann die zuvor registrierte Coroutine nach Namen nimmt (der Name wird aus dem Aufruf übernommen ), sie mit Argumenten aufruft (die auch aus dem Aufruf stammt ) und das Ergebnis der Ausführung dieser Coroutine wird ebenfalls im Aufruf gespeichert .



Objekte der Call- Klasse sind einfach Container zum Speichern von Informationen darüber, was und wie aufgerufen werden soll, und zum Speichern des Ergebnisses in sich selbst. Der Wrapper kann auch das Ergebnis der Vorlagenausführung zurückgeben. Dazu wird die Vorlage in eine spezielle Generator- Klasse eingeschlossen , die hier nicht angezeigt wird.



Ich habe einige der Nuancen weggelassen, aber ich hoffe, ich habe die Essenz im Allgemeinen vermittelt.



Um ehrlich zu sein, wurde dieser Artikel von mir geschrieben, um meine Gedanken zur Lösung von Problemen mit asynchronem Code in Python zu teilen .und vor allem, um den Meinungen der Bewohner von Chabrav zuzuhören. Vielleicht stoße ich jemanden auf eine andere Lösung, vielleicht ist jemand mit dieser speziellen Implementierung nicht einverstanden und sagt Ihnen, wie Sie sie verbessern können, vielleicht sagt Ihnen jemand, warum eine solche Lösung überhaupt nicht benötigt wird und Sie sollten nicht synchron und mischen Asynchroner Code, die Meinung eines jeden von Ihnen ist mir sehr wichtig. Außerdem gebe ich nicht vor, alle meine Überlegungen am Anfang des Artikels zu erfüllen. Ich habe sehr ausführlich über das Thema anderer YPs nachgedacht und könnte mich irren. Außerdem besteht die Möglichkeit, dass ich Konzepte verwirre, wenn Sie plötzlich Unstimmigkeiten bemerken - beschreiben Sie dies in den Kommentaren. Ich würde mich auch freuen, wenn es Änderungen an der Syntax und Interpunktion gibt.



Und vielen Dank für Ihre Aufmerksamkeit für dieses Thema und insbesondere für diesen Artikel!



All Articles