webentwicklung-frage-antwort-db.com.de

Pythonausbeute aus oder Rückgabe eines Generators?

Ich habe diesen einfachen Code geschrieben:

def mymap(func, *seq):
  return (func(*args) for args in Zip(*seq))

jetzt habe ich Zweifel, ist es besser, die 'return'-Anweisung wie oben für die Rückgabe eines Generators zu verwenden oder eine' yield from'-Anweisung wie diese zu verwenden:

def mymap(func, *seq):
 yield from (func(*args) for args in Zip(*seq))

und jenseits des technischen Unterschieds zwischen "Rendite" und "Rendite", welcher Ansatz ist im Allgemeinen der bessere?

17
AleMal

Der Unterschied besteht darin, dass Ihr erstes mymap nur eine übliche Funktion ist, in diesem Fall eine Fabrik, die einen Generator zurückgibt. Alles im Körper wird ausgeführt, sobald Sie die Funktion aufrufen.

def gen_factory(func, seq):
    """Generator factory returning a generator."""
    # do stuff ... immediately when factory gets called
    print("build generator & return")
    return (func(*args) for args in seq)

Das zweite mymap ist ebenfalls eine Fabrik, aber es ist auch ein Generator selbst, der von einem selbstgebauten Subgenerator im Inneren stammt. Da es sich um einen Generator handelt, beginnt die Ausführung des Körpers erst mit dem ersten Aufruf von next (generator).

def gen_generator(func, seq):
    """Generator yielding from sub-generator inside."""
    # do stuff ... first time when 'next' gets called
    print("build generator & yield")
    yield from (func(*args) for args in seq)

Ich denke, das folgende Beispiel wird es klarer machen. Wir definieren Datenpakete, die mit Funktionen verarbeitet werden sollen, gebündelt in Jobs, die wir an die Generatoren übergeben.

def add(a, b):
    return a + b

def sqrt(a):
    return a ** 0.5

data1 = [*Zip(range(1, 5))]  # [(1,), (2,), (3,), (4,)]
data2 = [(2, 1), (3, 1), (4, 1), (5, 1)]

job1 = (sqrt, data1)
job2 = (add, data2)

Jetzt führen wir den folgenden Code in einer interaktiven Shell wie IPython aus, um das unterschiedliche Verhalten zu sehen. gen_factory Wird sofort ausgedruckt, während gen_generator Dies erst nach dem Aufruf von next() tut.

gen_fac = gen_factory(*job1)
# build generator & return <-- printed immediately
next(gen_fac)  # start
# Out: 1.0
[*gen_fac]  # deplete rest of generator
# Out: [1.4142135623730951, 1.7320508075688772, 2.0]

gen_gen = gen_generator(*job1)
next(gen_gen)  # start
# build generator & yield <-- printed with first next()
# Out: 1.0
[*gen_gen]  # deplete rest of generator
# Out: [1.4142135623730951, 1.7320508075688772, 2.0]

Um Ihnen ein vernünftigeres Anwendungsbeispiel für ein Konstrukt wie gen_generator Zu geben, werden wir es ein wenig erweitern und eine Coroutine daraus machen, indem wir Variablen yield zuweisen, damit wir Jobs in den laufenden Generator mit send().

Zusätzlich erstellen wir eine Hilfsfunktion, die alle Aufgaben innerhalb eines Jobs ausführt und nach Abschluss nach einer neuen fragt.

def gen_coroutine():
    """Generator coroutine yielding from sub-generator inside."""
    # do stuff... first time when 'next' gets called
    print("receive job, build generator & yield, loop")
    while True:
        try:
            func, seq = yield "send me work ... or I quit with next next()"
        except TypeError:
            return "no job left"
        else:
            yield from (func(*args) for args in seq)


def do_job(gen, job):
    """Run all tasks in job."""
    print(gen.send(job))
    while True:
        result = next(gen)
        print(result)
        if result == "send me work ... or I quit with next next()":
            break

Jetzt führen wir gen_coroutine Mit unserer Hilfsfunktion do_job Und zwei Jobs aus.

gen_co = gen_coroutine()
next(gen_co)  # start
# receive job, build generator & yield, loop  <-- printed with first next()
# Out:'send me work ... or I quit with next next()'
do_job(gen_co, job1)  # prints out all results from job
# 1
# 1.4142135623730951
# 1.7320508075688772
# 2.0
# send me work... or I quit with next next()
do_job(gen_co, job2)  # send another job into generator
# 3
# 4
# 5
# 6
# send me work... or I quit with next next()
next(gen_co)
# Traceback ...
# StopIteration: no job left

Um auf Ihre Frage zurückzukommen, welche Version im Allgemeinen der bessere Ansatz ist. IMO etwas wie gen_factory Ist nur sinnvoll, wenn Sie dasselbe für mehrere Generatoren tun müssen, die Sie erstellen möchten, oder wenn Ihr Konstruktionsprozess für Generatoren so kompliziert ist, dass die Verwendung einer Fabrik anstelle des Baus einzelner Generatoren gerechtfertigt ist an Ort und Stelle mit einem Generator Verständnis.

Hinweis:

Die obige Beschreibung für die Funktion gen_generator (Zweites mymap) besagt "es ist ein Generator selbst". Das ist ein bisschen vage und technisch nicht richtig, erleichtert aber das Nachdenken über die Unterschiede der Funktionen in diesem kniffligen Setup, in dem gen_factory Auch einen Generator zurückgibt, nämlich den, der durch das Generatorverständnis im Inneren erstellt wurde.

In der Tat any function (nicht nur die aus dieser Frage mit Generatorverständnis!) With a yield inside, bei Aufruf nur return a generator Objekt, das aus dem Funktionskörper konstruiert wird.

type(gen_coroutine) # function
gen_co = gen_coroutine(); type(gen_co) # generator

Die gesamte Aktion, die wir oben für gen_generator Und gen_coroutine Beobachtet haben, findet in diesen Generatorobjekten statt. Funktionen mit yield inside wurden zuvor ausgespuckt.

13
Darkonaut

Generatoren use yield, Funktionen use return.

Generatoren werden im Allgemeinen in for-Schleifen für das wiederholte Durchlaufen der Werte verwendet. werden automatisch von einem Generator bereitgestellt, können aber auch in einem anderen Kontext verwendet werden, z. G. in list () Funktion zum Erstellen einer Liste - wieder aus Werten automatisch von einem Generator bereitgestellt .

Funktionen werden aufgerufen, um Rückgabewert , nur einen Wert für jeden Aufruf bereitzustellen.

0
MarianD