Lernen Sie über delegierende Generatoren

PythonPythonBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

In diesem Lab werden Sie lernen, wie man in Python Generatoren delegiert, indem man die Anweisung yield from verwendet. Diese Funktion, die in Python 3.3 eingeführt wurde, vereinfacht Code, der auf Generatoren und Koroutinen basiert.

Generatoren sind spezielle Funktionen, die die Ausführung anhalten und fortsetzen können und ihren Zustand zwischen den Aufrufen beibehalten. Die Anweisung yield from bietet eine elegante Möglichkeit, die Kontrolle an einen anderen Generator zu delegieren, was die Lesbarkeit und Wartbarkeit des Codes verbessert.

Ziele:

  • Verstehen Sie den Zweck der Anweisung yield from
  • Lernen Sie, wie man yield from verwendet, um an andere Generatoren zu delegieren
  • Wenden Sie diese Kenntnisse an, um auf Koroutinen basierenden Code zu vereinfachen
  • Verstehen Sie die Verbindung zur modernen async/await-Syntax

Dateien, mit denen Sie arbeiten werden:

  • cofollow.py - Enthält Hilfsfunktionen für Koroutinen
  • server.py - Enthält eine einfache Implementierung eines Netzwerkservers

Das yield from-Statement verstehen

In diesem Schritt werden wir das yield from-Statement in Python untersuchen. Dieses Statement ist ein leistungsstarkes Werkzeug, wenn man mit Generatoren arbeitet, und es vereinfacht den Prozess der Delegierung von Operationen an andere Generatoren. Am Ende dieses Schritts werden Sie verstehen, was yield from ist, wie es funktioniert und wie es den Wertetransfer zwischen verschiedenen Generatoren handhaben kann.

Was ist yield from?

Das yield from-Statement wurde in Python 3.3 eingeführt. Sein Hauptzweck ist es, die Delegierung von Operationen an Subgeneratoren zu vereinfachen. Ein Subgenerator ist einfach ein anderer Generator, an den ein Hauptgenerator die Arbeit delegieren kann.

Normalerweise, wenn Sie möchten, dass ein Generator Werte von einem anderen Generator ausgibt, müssen Sie eine Schleife verwenden. Beispielsweise würden Sie ohne yield from Code wie diesen schreiben:

def delegating_generator():
    for value in subgenerator():
        yield value

In diesem Code verwendet der delegating_generator eine for-Schleife, um über die von subgenerator erzeugten Werte zu iterieren und dann jeden Wert nacheinander auszugeben.

Mit dem yield from-Statement wird der Code jedoch viel einfacher:

def delegating_generator():
    yield from subgenerator()

Diese einzelne Codezeile erreicht dasselbe Ergebnis wie die Schleife im vorherigen Beispiel. Aber yield from ist nicht nur eine Abkürzung. Es verwaltet auch die bidirektionale Kommunikation zwischen dem Aufrufer und dem Subgenerator. Das bedeutet, dass alle an den delegierenden Generator gesendeten Werte direkt an den Subgenerator weitergeleitet werden.

Ein einfaches Beispiel

Lassen Sie uns ein einfaches Beispiel erstellen, um zu sehen, wie yield from in der Praxis funktioniert.

  1. Zunächst müssen wir die Datei cofollow.py im Editor öffnen. Dazu verwenden wir den Befehl cd, um in das richtige Verzeichnis zu navigieren. Führen Sie den folgenden Befehl im Terminal aus:
cd /home/labex/project
  1. Als Nächstes fügen wir zwei Funktionen zur Datei cofollow.py hinzu. Die Funktion subgen ist ein einfacher Generator, der die Zahlen von 0 bis 4 ausgibt. Die Funktion main_gen verwendet yield from, um die Generierung dieser Zahlen an subgen zu delegieren und gibt dann die Zeichenkette 'Done' aus. Fügen Sie den folgenden Code ans Ende der Datei cofollow.py hinzu:
def subgen():
    for i in range(5):
        yield i

def main_gen():
    yield from subgen()
    yield 'Done'
  1. Jetzt testen wir diese Funktionen. Öffnen Sie eine Python-Shell und führen Sie den folgenden Code aus:
from cofollow import subgen, main_gen

## Test subgen directly
for x in subgen():
    print(x)

## Test main_gen that delegates to subgen
for x in main_gen():
    print(x)

Wenn Sie diesen Code ausführen, sollten Sie die folgende Ausgabe sehen:

0
1
2
3
4

0
1
2
3
4
Done

Diese Ausgabe zeigt, dass yield from es main_gen ermöglicht, alle von subgen erzeugten Werte direkt an den Aufrufer zu übergeben.

Wertetransfer mit yield from

Eines der leistungsstärksten Merkmale von yield from ist seine Fähigkeit, den Wertetransfer in beide Richtungen zu handhaben. Lassen Sie uns ein komplexeres Beispiel erstellen, um dies zu demonstrieren.

  1. Fügen Sie die folgenden Funktionen zur Datei cofollow.py hinzu:
def accumulator():
    total = 0
    while True:
        value = yield total
        if value is None:
            break
        total += value

def caller():
    acc = accumulator()
    yield from acc
    yield 'Total accumulated'

Die Funktion accumulator ist eine Koroutine, die einen laufenden Gesamtwert verfolgt. Sie gibt den aktuellen Gesamtwert aus und wartet dann auf die Eingabe eines neuen Werts. Wenn sie None erhält, beendet sie die Schleife. Die Funktion caller erstellt eine Instanz von accumulator und verwendet yield from, um alle Sende- und Empfangsoperationen an sie zu delegieren.

  1. Testen Sie diese Funktionen in einer Python-Shell:
from cofollow import caller

c = caller()
print(next(c))  ## Start the coroutine
print(c.send(1))  ## Send value 1, get accumulated value
print(c.send(2))  ## Send value 2, get accumulated value
print(c.send(3))  ## Send value 3, get accumulated value
print(c.send(None))  ## Send None to exit the accumulator

Wenn Sie diesen Code ausführen, sollten Sie die folgende Ausgabe sehen:

0
1
3
6
'Total accumulated'

Diese Ausgabe zeigt, dass yield from alle Sende- und Empfangsoperationen vollständig an den Subgenerator delegiert, bis dieser erschöpft ist.

Nachdem Sie nun die Grundlagen von yield from verstanden haben, werden wir im nächsten Schritt zu praktischeren Anwendungen übergehen.

✨ Lösung prüfen und üben

Verwendung von yield from in Koroutinen

In diesem Schritt werden wir untersuchen, wie das yield from-Statement in Verbindung mit Koroutinen für praktischere Anwendungen eingesetzt werden kann. Koroutinen sind ein leistungsstarkes Konzept in Python, und das Verständnis der Verwendung von yield from mit ihnen kann Ihren Code erheblich vereinfachen.

Koroutinen und Nachrichtenübertragung

Koroutinen sind spezielle Funktionen, die Werte über die yield-Anweisung empfangen können. Sie sind äußerst nützlich für Aufgaben wie Datenverarbeitung und Ereignisbehandlung. In der Datei cofollow.py gibt es einen consumer-Decorator. Dieser Decorator hilft bei der Einrichtung von Koroutinen, indem er sie automatisch bis zum ersten yield-Punkt voranschreitet. Das bedeutet, dass Sie die Koroutine nicht manuell starten müssen; der Decorator erledigt dies für Sie.

Lassen Sie uns eine Koroutine erstellen, die Werte empfängt und deren Typen validiert. So können Sie es machen:

  1. Öffnen Sie zunächst die Datei cofollow.py im Editor. Sie können den folgenden Befehl im Terminal verwenden, um in das richtige Verzeichnis zu navigieren:
cd /home/labex/project
  1. Fügen Sie als Nächstes die folgende receive-Funktion am Ende der Datei cofollow.py hinzu. Diese Funktion ist eine Koroutine, die eine Nachricht empfängt und deren Typ validiert.
def receive(expected_type):
    """
    A coroutine that receives a message and validates its type.
    Returns the received message if it matches the expected type.
    """
    msg = yield
    assert isinstance(msg, expected_type), f'Expected type {expected_type}'
    return msg

Was diese Funktion tut:

  • Sie verwendet yield ohne Ausdruck, um einen Wert zu empfangen. Wenn der Koroutine ein Wert gesendet wird, wird dieser von der yield-Anweisung erfasst.
  • Sie überprüft, ob der empfangene Wert vom erwarteten Typ ist, indem sie die isinstance-Funktion verwendet. Wenn der Typ nicht übereinstimmt, wird ein AssertionError ausgelöst.
  • Wenn die Typüberprüfung erfolgreich ist, wird der Wert zurückgegeben.
  1. Jetzt erstellen wir eine Koroutine, die yield from mit unserer receive-Funktion verwendet. Diese neue Koroutine wird nur Ganzzahlen empfangen und ausgeben.
@consumer
def print_ints():
    """
    A coroutine that receives and prints integers only.
    Uses yield from to delegate to the receive coroutine.
    """
    while True:
        val = yield from receive(int)
        print('Got:', val)
  1. Um diese Koroutine zu testen, öffnen Sie eine Python-Shell und führen Sie den folgenden Code aus:
from cofollow import print_ints

p = print_ints()
p.send(42)
p.send(13)
try:
    p.send('13')  ## This should raise an AssertionError
except AssertionError as e:
    print(f"Error: {e}")

Sie sollten die folgende Ausgabe sehen:

Got: 42
Got: 13
Error: Expected type <class 'int'>

Verständnis der Funktionsweise von yield from mit Koroutinen

Wenn wir yield from receive(int) in der print_ints-Koroutine verwenden, erfolgen die folgenden Schritte:

  1. Die Kontrolle wird an die receive-Koroutine delegiert. Das bedeutet, dass die print_ints-Koroutine anhält und die receive-Koroutine beginnt auszuführen.
  2. Die receive-Koroutine verwendet yield, um einen Wert zu empfangen. Sie wartet darauf, dass ihr ein Wert gesendet wird.
  3. Wenn ein Wert an print_ints gesendet wird, wird er tatsächlich von receive empfangen. Das yield from-Statement kümmert sich darum, den Wert von print_ints an receive zu übergeben.
  4. Die receive-Koroutine validiert den Typ des empfangenen Werts. Wenn der Typ korrekt ist, wird der Wert zurückgegeben.
  5. Der zurückgegebene Wert wird zum Ergebnis des yield from-Ausdrucks in der print_ints-Koroutine. Das bedeutet, dass die Variable val in print_ints den von receive zurückgegebenen Wert zugewiesen bekommt.

Die Verwendung von yield from macht den Code lesbarer als wenn wir das Yielding und Empfangen direkt handhaben müssten. Es abstrahiert die Komplexität des Wertetransfers zwischen Koroutinen.

Erstellung fortschrittlicherer typprüfender Koroutinen

Lassen Sie uns unsere Hilfsfunktionen erweitern, um komplexere Typvalidierungen zu behandeln. So können Sie es machen:

  1. Fügen Sie die folgenden Funktionen zur Datei cofollow.py hinzu:
def receive_dict():
    """Receive and validate a dictionary"""
    result = yield from receive(dict)
    return result

def receive_str():
    """Receive and validate a string"""
    result = yield from receive(str)
    return result

@consumer
def process_data():
    """Process different types of data using the receive utilities"""
    while True:
        print("Waiting for a string...")
        name = yield from receive_str()
        print(f"Got string: {name}")

        print("Waiting for a dictionary...")
        data = yield from receive_dict()
        print(f"Got dictionary with {len(data)} items: {data}")

        print("Processing complete for this round.")
  1. Um die neue Koroutine zu testen, öffnen Sie eine Python-Shell und führen Sie den folgenden Code aus:
from cofollow import process_data

proc = process_data()
proc.send("John Doe")
proc.send({"age": 30, "city": "New York"})
proc.send("Jane Smith")
try:
    proc.send(123)  ## This should raise an AssertionError
except AssertionError as e:
    print(f"Error: {e}")

Sie sollten eine Ausgabe wie diese sehen:

Waiting for a string...
Got string: John Doe
Waiting for a dictionary...
Got dictionary with 2 items: {'age': 30, 'city': 'New York'}
Processing complete for this round.
Waiting for a string...
Got string: Jane Smith
Waiting for a dictionary...
Error: Expected type <class 'dict'>

Das yield from-Statement macht den Code sauberer und lesbarer. Es ermöglicht es uns, uns auf die Hochschul-logik unseres Programms zu konzentrieren, anstatt uns in die Details der Nachrichtenübertragung zwischen Koroutinen zu verlieren.

✨ Lösung prüfen und üben

Einwickeln von Sockets mit Generatoren

In diesem Schritt werden wir lernen, wie man Generatoren verwendet, um Socket-Operationen zu kapseln. Dies ist ein sehr wichtiges Konzept, insbesondere im Bereich der asynchronen Programmierung. Asynchrone Programmierung ermöglicht es Ihrem Programm, mehrere Aufgaben gleichzeitig zu bearbeiten, ohne auf das Ende einer Aufgabe zu warten, bevor eine andere gestartet wird. Die Verwendung von Generatoren zur Kapselung von Socket-Operationen kann Ihren Code effizienter und leichter zu verwalten machen.

Das Problem verstehen

Die Datei server.py enthält eine einfache Implementierung eines Netzwerkservers unter Verwendung von Generatoren. Schauen wir uns den aktuellen Code an. Dieser Code ist die Grundlage unseres Servers, und es ist wichtig, ihn zu verstehen, bevor wir irgendwelche Änderungen vornehmen.

def tcp_server(address, handler):
    sock = socket(AF_INET, SOCK_STREAM)
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)
    while True:
        yield 'recv', sock
        client, addr = sock.accept()
        tasks.append(handler(client, addr))

def echo_handler(client, address):
    print('Connection from', address)
    while True:
        yield 'recv', client
        data = client.recv(1000)
        if not data:
            break
        yield 'send', client
        client.send(b'GOT:' + data)
    print('Connection closed')
    client.close()

In diesem Code verwenden wir das Schlüsselwort yield. Das Schlüsselwort yield wird in Python verwendet, um Generatoren zu erstellen. Ein Generator ist ein spezieller Typ von Iterator, der es Ihnen ermöglicht, die Ausführung einer Funktion anzuhalten und fortzusetzen. Hier wird yield verwendet, um anzuzeigen, wann der Server bereit ist, eine Verbindung zu akzeptieren oder wann ein Client-Handler bereit ist, Daten zu empfangen oder zu senden. Allerdings machen die manuellen yield-Anweisungen die internen Funktionsweisen der Ereignisschleife (event loop) dem Benutzer zugänglich. Das bedeutet, dass der Benutzer wissen muss, wie die Ereignisschleife funktioniert, was den Code schwieriger zu verstehen und zu warten macht.

Erstellen einer GenSocket-Klasse

Lassen Sie uns eine GenSocket-Klasse erstellen, um Socket-Operationen mit Generatoren zu kapseln. Dies wird unseren Code sauberer und lesbarer machen. Indem wir die Socket-Operationen in einer Klasse kapseln, können wir die Details der Ereignisschleife vor dem Benutzer verbergen und uns auf die Hochschul-logik des Servers konzentrieren.

  1. Öffnen Sie die Datei server.py im Editor:
cd /home/labex/project

Dieser Befehl wechselt das aktuelle Verzeichnis in das Projektverzeichnis, in dem sich die Datei server.py befindet. Sobald Sie sich im richtigen Verzeichnis befinden, können Sie die Datei in Ihrem bevorzugten Texteditor öffnen.

  1. Fügen Sie die folgende GenSocket-Klasse am Ende der Datei, vor allen bestehenden Funktionen, hinzu:
class GenSocket:
    """
    A generator-based wrapper for socket operations.
    """
    def __init__(self, sock):
        self.sock = sock

    def accept(self):
        """Accept a connection and return a new GenSocket"""
        yield 'recv', self.sock
        client, addr = self.sock.accept()
        return GenSocket(client), addr

    def recv(self, maxsize):
        """Receive data from the socket"""
        yield 'recv', self.sock
        return self.sock.recv(maxsize)

    def send(self, data):
        """Send data to the socket"""
        yield 'send', self.sock
        return self.sock.send(data)

    def __getattr__(self, name):
        """Forward any other attributes to the underlying socket"""
        return getattr(self.sock, name)

Diese GenSocket-Klasse fungiert als Wrapper für Socket-Operationen. Die __init__-Methode initialisiert die Klasse mit einem Socket-Objekt. Die Methoden accept, recv und send führen die entsprechenden Socket-Operationen aus und verwenden yield, um anzuzeigen, wann die Operation bereit ist. Die __getattr__-Methode ermöglicht es der Klasse, alle anderen Attribute an das zugrunde liegende Socket-Objekt weiterzuleiten.

  1. Modifizieren Sie nun die Funktionen tcp_server und echo_handler, um die GenSocket-Klasse zu verwenden:
def tcp_server(address, handler):
    sock = GenSocket(socket(AF_INET, SOCK_STREAM))
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)
    while True:
        client, addr = yield from sock.accept()
        tasks.append(handler(client, addr))

def echo_handler(client, address):
    print('Connection from', address)
    while True:
        data = yield from client.recv(1000)
        if not data:
            break
        yield from client.send(b'GOT:' + data)
    print('Connection closed')
    client.close()

Beachten Sie, wie die expliziten yield 'recv', sock- und yield 'send', client-Anweisungen durch sauberere yield from-Ausdrücke ersetzt wurden. Das Schlüsselwort yield from wird verwendet, um die Ausführung an einen anderen Generator zu delegieren. Dies macht den Code lesbarer und verbirgt die Details der Ereignisschleife vor dem Benutzer. Jetzt sieht der Code eher wie normale Funktionsaufrufe aus, und der Benutzer muss sich nicht um die internen Funktionsweisen der Ereignisschleife kümmern.

  1. Fügen wir eine einfache Testfunktion hinzu, um zu demonstrieren, wie unser Server verwendet wird:
def run_server():
    """Start the server on port 25000"""
    tasks.append(tcp_server(('localhost', 25000), echo_handler))
    try:
        event_loop()
    except KeyboardInterrupt:
        print("Server stopped")

if __name__ == '__main__':
    print("Starting echo server on port 25000...")
    print("Press Ctrl+C to stop")
    run_server()

Dieser Code ist lesbarer und leichter zu warten. Die GenSocket-Klasse kapselt die Yield-Logik, sodass der Servercode sich auf den Hochschulfluss statt auf die Details der Ereignisschleife konzentrieren kann. Die run_server-Funktion startet den Server auf Port 25000 und behandelt die KeyboardInterrupt-Ausnahme, die es dem Benutzer ermöglicht, den Server durch Drücken von Ctrl+C zu stoppen.

Die Vorteile verstehen

Der yield from-Ansatz bietet mehrere Vorteile:

  1. Sauberer Code: Die Socket-Operationen sehen eher wie normale Funktionsaufrufe aus. Dies macht den Code leichter zu lesen und zu verstehen, insbesondere für Anfänger.
  2. Abstraktion: Die Details der Ereignisschleife sind vor dem Benutzer verborgen. Der Benutzer muss nicht wissen, wie die Ereignisschleife funktioniert, um den Servercode zu verwenden.
  3. Lesbarkeit: Der Code drückt eher aus, was er tut, als wie er es tut. Dies macht den Code selbstverständlicher und leichter zu warten.
  4. Wartbarkeit: Änderungen an der Ereignisschleife erfordern keine Änderungen am Servercode. Das bedeutet, dass Sie die Ereignisschleife in Zukunft ändern können, ohne den Servercode zu beeinflussen.

Dieses Muster ist ein Sprungbrett zu der modernen async/await-Syntax, die wir im nächsten Schritt untersuchen werden. Die async/await-Syntax ist eine fortschrittlichere und sauberere Art, asynchronen Code in Python zu schreiben, und das Verständnis des yield from-Musters wird Ihnen helfen, sich leichter auf sie umzustellen.

✨ Lösung prüfen und üben

Von Generatoren zu Async/Await

In diesem letzten Schritt werden wir untersuchen, wie sich das yield from-Muster in Python zur modernen async/await-Syntax entwickelt hat. Das Verständnis dieser Entwicklung ist von entscheidender Bedeutung, da es Ihnen hilft, die Verbindung zwischen Generatoren und asynchroner Programmierung zu erkennen. Asynchrone Programmierung ermöglicht es Ihrem Programm, mehrere Aufgaben zu bearbeiten, ohne auf das Ende jeder einzelnen Aufgabe zu warten. Dies ist besonders nützlich in der Netzwerkprogrammierung und anderen I/O - gebundenen Operationen.

Die Verbindung zwischen Generatoren und Async/Await

Die async/await-Syntax, die in Python 3.5 eingeführt wurde, baut auf der Generator- und yield from-Funktionalität auf. Im Hintergrund werden async-Funktionen mithilfe von Generatoren implementiert. Das bedeutet, dass die Konzepte, die Sie über Generatoren gelernt haben, direkt mit der Funktionsweise von async/await zusammenhängen.

Um von der Verwendung von Generatoren zur async/await-Syntax zu wechseln, müssen wir die folgenden Schritte befolgen:

  1. Verwenden Sie den @coroutine-Decorator aus dem types-Modul. Dieser Decorator hilft dabei, generatorbasierte Funktionen in eine Form zu bringen, die mit async/await verwendet werden kann.
  2. Konvertieren Sie Funktionen, die yield from verwenden, so dass sie stattdessen async und await nutzen. Dies macht den Code lesbarer und bringt die asynchrone Natur der Operationen besser zum Ausdruck.
  3. Aktualisieren Sie die Ereignisschleife (event loop), um native Koroutinen zu verarbeiten. Die Ereignisschleife ist für die Planung und Ausführung asynchroner Aufgaben verantwortlich.

Aktualisieren der GenSocket-Klasse

Jetzt modifizieren wir unsere GenSocket-Klasse, damit sie mit dem @coroutine-Decorator funktioniert. Dies ermöglicht es uns, unsere Klasse in einem async/await-Kontext zu verwenden.

  1. Öffnen Sie die Datei server.py im Editor. Sie können dies tun, indem Sie den folgenden Befehl im Terminal ausführen:
cd /home/labex/project
  1. Fügen Sie am Anfang der Datei server.py den Import für coroutine hinzu. Dieser Import ist erforderlich, um den @coroutine-Decorator zu verwenden.
from types import coroutine
  1. Aktualisieren Sie die GenSocket-Klasse, um den @coroutine-Decorator zu verwenden. Dieser Decorator wandelt unsere generatorbasierten Methoden in ausführbare Koroutinen um, was bedeutet, dass sie mit dem await-Schlüsselwort verwendet werden können.
class GenSocket:
    """
    A generator-based wrapper for socket operations
    that works with async/await.
    """
    def __init__(self, sock):
        self.sock = sock

    @coroutine
    def accept(self):
        """Accept a connection and return a new GenSocket"""
        yield 'recv', self.sock
        client, addr = self.sock.accept()
        return GenSocket(client), addr

    @coroutine
    def recv(self, maxsize):
        """Receive data from the socket"""
        yield 'recv', self.sock
        return self.sock.recv(maxsize)

    @coroutine
    def send(self, data):
        """Send data to the socket"""
        yield 'send', self.sock
        return self.sock.send(data)

    def __getattr__(self, name):
        """Forward any other attributes to the underlying socket"""
        return getattr(self.sock, name)

Umwandlung in die Async/Await-Syntax

Als Nächstes wandeln wir unseren Servercode um, um die async/await-Syntax zu verwenden. Dies macht den Code lesbarer und bringt die asynchrone Natur der Operationen klar zum Ausdruck.

async def tcp_server(address, handler):
    """
    An asynchronous TCP server using async/await.
    """
    sock = GenSocket(socket(AF_INET, SOCK_STREAM))
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)
    while True:
        client, addr = await sock.accept()
        tasks.append(handler(client, addr))

async def echo_handler(client, address):
    """
    An asynchronous handler for echo clients.
    """
    print('Connection from', address)
    while True:
        data = await client.recv(1000)
        if not data:
            break
        await client.send(b'GOT:' + data)
    print('Connection closed')
    client.close()

Beachten Sie, dass yield from durch await ersetzt wurde und die Funktionen jetzt mit async def anstelle von def definiert werden. Diese Änderung macht den Code intuitiver und leichter zu verstehen.

Das Verständnis der Transformation

Der Übergang von Generatoren mit yield from zur async/await-Syntax ist nicht nur eine einfache syntaktische Änderung. Er repräsentiert eine Verschiebung in der Art und Weise, wie wir über asynchrone Programmierung denken.

  1. Generatoren mit yield from:

    • Wenn Sie Generatoren mit yield from verwenden, geben Sie explizit die Kontrolle ab, um anzuzeigen, dass eine Aufgabe bereit ist. Das bedeutet, dass Sie manuell verwalten müssen, wann eine Aufgabe fortgesetzt werden kann.
    • Sie müssen auch die Planung der Aufgaben manuell verwalten. Dies kann komplex sein, insbesondere in größeren Programmen.
    • Der Schwerpunkt liegt auf der Mechanik des Kontrollflusses, was den Code schwieriger zu lesen und zu warten machen kann.
  2. Async/await-Syntax:

    • Mit der async/await-Syntax wird die Kontrolle implizit an await-Punkten abgegeben. Dies macht den Code einfacher, da Sie sich nicht um das explizite Abgeben der Kontrolle kümmern müssen.
    • Die Ereignisschleife kümmert sich um die Planung der Aufgaben, sodass Sie dies nicht manuell verwalten müssen.
    • Der Schwerpunkt liegt auf dem logischen Fluss des Programms, was den Code lesbarer und wartbarer macht.

Diese Transformation ermöglicht einen lesbareren und wartbareren asynchronen Code, was besonders wichtig für komplexe Anwendungen wie Netzwerkserver ist.

Moderne asynchrone Programmierung

In modernem Python verwenden wir normalerweise das asyncio-Modul für asynchrone Programmierung anstelle einer benutzerdefinierten Ereignisschleife. Das asyncio-Modul bietet integrierte Unterstützung für viele nützliche Funktionen:

  • Gleichzeitiges Ausführen mehrerer Koroutinen. Dies ermöglicht es Ihrem Programm, mehrere Aufgaben gleichzeitig zu bearbeiten.
  • Verwaltung von Netzwerk-I/O. Es vereinfacht den Prozess des Senden und Empfangens von Daten über das Netzwerk.
  • Synchronisierungsprimitive. Diese helfen Ihnen, den Zugang zu gemeinsamen Ressourcen in einer parallelen Umgebung zu verwalten.
  • Aufgabenplanung und -abbruch. Sie können Aufgaben einfach zu bestimmten Zeiten planen und bei Bedarf abbrechen.

So könnte unser Server unter Verwendung von asyncio aussehen:

import asyncio

async def handle_client(reader, writer):
    addr = writer.get_extra_info('peername')
    print(f'Connection from {addr}')

    while True:
        data = await reader.read(1000)
        if not data:
            break

        writer.write(b'GOT:' + data)
        await writer.drain()

    print('Connection closed')
    writer.close()
    await writer.wait_closed()

async def main():
    server = await asyncio.start_server(
        handle_client, 'localhost', 25000
    )

    addr = server.sockets[0].getsockname()
    print(f'Serving on {addr}')

    async with server:
        await server.serve_forever()

if __name__ == '__main__':
    asyncio.run(main())

Dieser Code erreicht die gleiche Funktionalität wie unser generatorbasierter Server, verwendet jedoch die Standardbibliothek asyncio, die robuster und featurereicher ist.

Fazit

In diesem Lab haben Sie mehrere wichtige Konzepte gelernt:

  1. Die yield from-Anweisung und wie sie die Ausführung an einen anderen Generator delegiert. Dies ist ein grundlegendes Konzept für das Verständnis der Funktionsweise von Generatoren.
  2. Wie man yield from mit Koroutinen für die Nachrichtenübertragung verwendet. Dies ermöglicht es Ihnen, zwischen verschiedenen Teilen Ihres asynchronen Programms zu kommunizieren.
  3. Das Einwickeln von Socket-Operationen mit Generatoren für einen saubereren Code. Dies macht Ihren netzwerkbezogenen Code besser organisiert und leichter zu verstehen.
  4. Der Übergang von Generatoren zur modernen async/await-Syntax. Das Verständnis dieses Übergangs wird Ihnen helfen, lesbareren und wartbareren asynchronen Code in Python zu schreiben, unabhängig davon, ob Sie direkt Generatoren oder die moderne async/await-Syntax verwenden.
✨ Lösung prüfen und üben

Zusammenfassung

In diesem Lab haben Sie das Konzept des Delegierens von Generatoren in Python kennengelernt, wobei der Schwerpunkt auf der yield from-Anweisung und ihren verschiedenen Anwendungen lag. Sie haben untersucht, wie man yield from verwendet, um die Ausführung an einen anderen Generator zu delegieren, was den Code vereinfacht und die Lesbarkeit verbessert. Sie haben auch gelernt, wie man mit yield from Koroutinen erstellt, um Nachrichten zu empfangen und zu validieren, und wie man Generatoren verwendet, um Socket-Operationen zu kapseln und so einen saubereren Netzwerkcode zu erhalten.

Diese Konzepte sind von entscheidender Bedeutung für das Verständnis der asynchronen Programmierung in Python. Der Übergang von Generatoren zur modernen async/await-Syntax stellt einen bedeutenden Fortschritt bei der Bearbeitung asynchroner Operationen dar. Um diese Konzepte weiter zu erkunden, können Sie das asyncio-Modul studieren, untersuchen, wie beliebte Frameworks async/await verwenden, und Ihre eigenen asynchronen Bibliotheken entwickeln. Das Verständnis von delegierenden Generatoren und yield from bietet einen tieferen Einblick in Python's Vorgehensweise bei der asynchronen Programmierung.