Konvertieren der Anweisung "Yield from" in Python 2.7-Code


73

Ich hatte unten einen Code in Python 3.2 und wollte ihn in Python 2.7 ausführen. Ich habe es konvertiert (habe den Code missing_elementsin beiden Versionen eingegeben), bin mir aber nicht sicher, ob dies der effizienteste Weg ist. Was passiert grundsätzlich, wenn zwei yield fromAufrufe wie unten in der oberen und unteren Hälfte missing_elementfunktionieren? Werden die Einträge aus den beiden Hälften (obere und untere) in einer Liste aneinander angehängt, sodass die übergeordnete Rekursion beim yield fromAufruf funktioniert und beide Hälften zusammen verwendet werden?

def missing_elements(L, start, end):  # Python 3.2
    if end - start <= 1: 
        if L[end] - L[start] > 1:
            yield from range(L[start] + 1, L[end])
        return

index = start + (end - start) // 2

# is the lower half consecutive?
consecutive_low =  L[index] == L[start] + (index - start)
if not consecutive_low:
    yield from missing_elements(L, start, index)

# is the upper part consecutive?
consecutive_high =  L[index] == L[end] - (end - index)
if not consecutive_high:
    yield from missing_elements(L, index, end)

def main():
    L = [10, 11, 13, 14, 15, 16, 17, 18, 20]
    print(list(missing_elements(L, 0, len(L)-1)))
    L = range(10, 21)
    print(list(missing_elements(L, 0, len(L)-1)))

def missing_elements(L, start, end):  # Python 2.7
    return_list = []                
    if end - start <= 1: 
        if L[end] - L[start] > 1:
            return range(L[start] + 1, L[end])

    index = start + (end - start) // 2

    # is the lower half consecutive?
    consecutive_low =  L[index] == L[start] + (index - start)
    if not consecutive_low:
        return_list.append(missing_elements(L, start, index))

    # is the upper part consecutive?
    consecutive_high =  L[index] == L[end] - (end - index)
    if not consecutive_high:
        return_list.append(missing_elements(L, index, end))
    return return_list

Die meisten der folgenden Implementierungen werden in gewisser Hinsicht nicht unterstützt (zum Senden von Werten an Generatoren, zum Behandeln verschachtelter Erträge usw.). Ich habe ein Paket in PyPI veröffentlicht, das versucht, das Verhalten umfassend zu gestalten: amir.rachum.com/yieldfrom
Amir Rachum

Antworten:


92

Wenn Sie die Ergebnisse Ihrer Erträge nicht verwenden, * können Sie dies jederzeit ändern:

yield from foo

… das mögen:

for bar in foo:
    yield bar

Es kann Leistungskosten geben, ** aber es gibt nie einen semantischen Unterschied.


Werden die Einträge aus den beiden Hälften (obere und untere) in einer Liste aneinander angehängt, sodass die übergeordnete Rekursion mit der Ausbeute aus dem Aufruf funktioniert und beide Hälften zusammen verwendet?

Nein! Der springende Punkt bei Iteratoren und Generatoren ist, dass Sie keine tatsächlichen Listen erstellen und diese zusammenfügen.

Aber der Effekt ist ähnlich: Sie geben nur von einem nach, dann von einem anderen.

Wenn Sie sich die obere und die untere Hälfte als "faule Listen" vorstellen, dann können Sie sich dies als "faulen Anhang" vorstellen, der eine größere "faule Liste" erstellt. Und wenn Sie rufen listauf das Ergebnis der Funktion Eltern, können Sie natürlich wird eine tatsächliche erhalten list, die das Anhängen zusammen die beiden Listen gleichwertig ist Sie bekommen hätte , wenn Sie getan hatte yield list(…)statt yield from ….

Aber ich denke, es ist einfacher, es anders herum zu sehen: Was es tut, ist genau das gleiche, was die forSchleifen tun.

Wenn Sie die beiden Iteratoren in Variablen gespeichert und eine Schleife durchlaufen itertools.chain(upper, lower)hätten, wäre dies dasselbe wie eine Schleife über die erste und eine Schleife über die zweite, oder? Kein Unterschied hier. In der Tat könnten Sie wie folgt implementieren chain:

for arg in *args:
    yield from arg

* Nicht die Werte, die der Generator seinem Aufrufer liefert, sondern der Wert der Ertragsausdrücke selbst innerhalb des Generators (die vom Aufrufer mithilfe der sendMethode stammen), wie in PEP 342 beschrieben . Sie verwenden diese in Ihren Beispielen nicht. Und ich bin bereit zu wetten, dass Sie nicht in Ihrem richtigen Code sind. Code im Coroutine-Stil verwendet jedoch häufig den Wert eines yield fromAusdrucks - Beispiele finden Sie in PEP 3156 . Ein solcher Code hängt normalerweise von anderen Funktionen der Python 3.3-Generatoren ab, insbesondere von den neuen Funktionen StopIteration.valuedes gleichen PEP 380 , die eingeführt wurdenyield from- also muss es neu geschrieben werden. Wenn nicht, können Sie das PEP verwenden, das Ihnen auch das komplette schreckliche, unordentliche Äquivalent zeigt, und Sie können natürlich die Teile reduzieren, die Sie nicht interessieren. Wenn Sie den Wert des Ausdrucks nicht verwenden, wird er auf die beiden obigen Zeilen reduziert.

** Keine große, und Sie können nichts dagegen tun, außer Python 3.3 zu verwenden oder Ihren Code vollständig umzustrukturieren. Dies ist genau der gleiche Fall wie das Übersetzen von Listenverständnissen in Python 1.5-Schleifen oder ein anderer Fall, in dem Version XY neu optimiert wurde und Sie eine ältere Version verwenden müssen.


Eine Frage, wie die rekursiven Aufrufe funktionieren? Kombiniert die übergeordnete Funktion "Ertrag von" zwei Anweisungen "Ertrag von" im untergeordneten Element? wenn nicht konsekutiv_low: Ausbeute aus fehlenden_Elementen (L, Start, Index) # ist der obere Teil aufeinanderfolgend? konsekutiv_hoch = L [Index] == L [Ende] - (Ende - Index) wenn nicht konsekutiv_hoch: Ausbeute aus fehlenden_Elementen (L, Index, Ende)
vkaul11

1
@ vkaul11: Es funktioniert genau wie die Schleife, nur schneller (und unterstützt verschiedene komplexere Fälle, die die Schleife nicht kann). Wenn Sie die wichtigsten Details erfahren möchten, lesen Sie den PEP.
Abarnert

@abarnet Verwendet der Code nicht das Ergebnis der Ausbeute aus den nachfolgenden Rekursionsaufrufen? Ich habe versucht, den Code von jemandem bei der Arbeit zu verwenden und in Python 2.7 auszuführen. Deshalb wollte ich verstehen, warum Sie das sagen. Hat eine "Ausbeute von recursive_function ()" nur eine Schleife über jede verschachtelte Ausbeute in der recursive_function
vkaul11

@ vkaul11: Nicht die Werte, die vom Generator an den Aufrufer ausgegeben wurden, sondern die Werte der tatsächlichen yieldAusdrücke innerhalb des Generators. Das ist schwer zu erklären; Siehe PEP 342, wenn Sie es verstehen wollen, aber kurz: Wenn Sie niemals sendeinen Generator anrufen oder dies niemals foo = (yield bar)im Generator tun und sich nicht vorstellen können, warum Sie dies jemals tun möchten, machen Sie sich bis dahin keine Sorgen Sie haben Zeit, PEP 342 (und 380 und 3156 sowie Greg Ewings raffinierte Blog-Beiträge von 3156) zu lesen.
Abarnert

1
@trss: Nein, immer noch das gleiche. Beide Formen werden nichts tun. Vergleichen Sie dies und das ; beide ergeben nichts, also drucken sie aus doneund erhöhen dann StopIteration. (Sie markieren auch sowohl die Funktion als Generator Funktion trotz nichts ergeben, dh Sie können yield from ()statt if False: yield Noneeinen Generator zu erzwingen.)
abarnert

7

Ich bin gerade auf dieses Problem gestoßen und meine Verwendung war etwas schwieriger, da ich den Rückgabewert von yield from: benötigte.

result = yield from other_gen()

Dies kann nicht als einfache forSchleife dargestellt werden, sondern kann folgendermaßen reproduziert werden:

_iter = iter(other_gen())
try:
    while True: #broken by StopIteration
        yield next(_iter)
except StopIteration as e:
    if e.args:
        result = e.args[0]
    else:
        result = None

Hoffentlich hilft dies Menschen, die auf das gleiche Problem stoßen. :) :)


6

Ersetzen Sie sie durch for-Schleifen:

yield from range(L[start] + 1, L[end])

==>

for i in range(L[start] + 1, L[end]):
    yield i

Das gleiche gilt für Elemente:

yield from missing_elements(L, index, end)

==>

for el in missing_elements(L, index, end):
    yield el

3

Ich glaube, ich habe einen Weg gefunden, das Python 3.x- yield fromKonstrukt in Python 2.x zu emulieren . Es ist nicht effizient und ein bisschen hackig, aber hier ist es:

import types

def inline_generators(fn):
    def inline(value):
        if isinstance(value, InlineGenerator):
            for x in value.wrapped:
                for y in inline(x):
                    yield y
        else:
            yield value
    def wrapped(*args, **kwargs):
        result = fn(*args, **kwargs)
        if isinstance(result, types.GeneratorType):
            result = inline(_from(result))
        return result
    return wrapped

class InlineGenerator(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped

def _from(value):
    assert isinstance(value, types.GeneratorType)
    return InlineGenerator(value)

Verwendung:

@inline_generators
def outer(x):
    def inner_inner(x):
        for x in range(1, x + 1):
            yield x
    def inner(x):
        for x in range(1, x + 1):
            yield _from(inner_inner(x))
    for x in range(1, x + 1):
        yield _from(inner(x))

for x in outer(3):
    print x,

Erzeugt Ausgabe:

1 1 1 2 1 1 2 1 2 3

Vielleicht findet das jemand hilfreich.

Bekannte Probleme: Es fehlt die Unterstützung für send () und verschiedene in PEP 380 beschriebene Eckfälle. Diese können hinzugefügt werden, und ich werde meinen Eintrag bearbeiten, sobald er funktioniert.


6
Was ist der Vorteil dieser Lösung gegenüber der früheren einfachen Lösung von Abernert, bei der in eine for-Schleife konvertiert wird?
ToolmakerSteve

Dies muss ein ActiveState-Rezept sein.
kirbyfan64sos

Schöne Umsetzung. Ich möchte nur darauf hinweisen, dass das Trollius-Projekt (asyncio für Python <3.3) dasselbe mit einer FromMethode tut . Die Umsetzung ist sicherlich produktionsbereit.
Jsbueno

Vielen Dank, es sollte die akzeptierte Antwort sein. Ich weiß, dass ich dies auf abarnerts Weise tun kann. Aber ich möchte einen effektiveren Weg finden, deshalb komme ich auf diese Seite.
Yunfan

3

Wie wäre es mit der Definition von pep-380, um eine Python 2-Syntaxversion zu erstellen:

Die Aussage:

RESULT = yield from EXPR

ist semantisch äquivalent zu:

_i = iter(EXPR)
try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value
else:
    while 1:
        try:
            _s = yield _y
        except GeneratorExit as _e:
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        except BaseException as _e:
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else:
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:
            try:
                if _s is None:
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:
                _r = _e.value
                break
RESULT = _r

In einem Generator lautet die Anweisung:

return value

ist semantisch äquivalent zu

raise StopIteration(value)

mit der Ausnahme, dass die Ausnahme wie derzeit nicht durch exceptKlauseln im zurückkehrenden Generator abgefangen werden kann .

Die StopIteration-Ausnahme verhält sich wie folgt definiert:

class StopIteration(Exception):

    def __init__(self, *args):
        if len(args) > 0:
            self.value = args[0]
        else:
            self.value = None
        Exception.__init__(self, *args)

0

Ich habe festgestellt, dass die Verwendung von Ressourcenkontexten (mithilfe des Python-Ressourcenmoduls ) ein eleganter Mechanismus für die Implementierung von Subgeneratoren in Python 2.7 ist. Praktischerweise hatte ich die Ressourcenkontexte sowieso schon verwendet.

Wenn Sie in Python 3.3 Folgendes hätten:

@resources.register_func
def get_a_thing(type_of_thing):
    if type_of_thing is "A":
        yield from complicated_logic_for_handling_a()
    else:
        yield from complicated_logic_for_handling_b()

def complicated_logic_for_handling_a():
    a = expensive_setup_for_a()
    yield a
    expensive_tear_down_for_a()

def complicated_logic_for_handling_b():
    b = expensive_setup_for_b()
    yield b
    expensive_tear_down_for_b()

In Python 2.7 hätten Sie:

@resources.register_func
def get_a_thing(type_of_thing):
    if type_of_thing is "A":
        with resources.complicated_logic_for_handling_a_ctx() as a:
            yield a
    else:
        with resources.complicated_logic_for_handling_b_ctx() as b:
            yield b

@resources.register_func
def complicated_logic_for_handling_a():
    a = expensive_setup_for_a()
    yield a
    expensive_tear_down_for_a()

@resources.register_func
def complicated_logic_for_handling_b():
    b = expensive_setup_for_b()
    yield b
    expensive_tear_down_for_b()

Beachten Sie, dass für die Operationen mit komplizierter Logik nur die Registrierung als Ressource erforderlich ist.


Wenn das einzige, was Ihr Generator tut, ein yield fromanderer Generator ist (genau einmal immer), können Sie stattdessen einfach diesen Generator zurückgeben. get_a_thingkönnte beide yield fromdurch ein ersetzen returnund es würde genauso gut funktionieren.
Tadhg McDonald-Jensen
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.