Anfängerfrage zu yield

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Inatrox
User
Beiträge: 6
Registriert: Montag 26. Juni 2023, 12:44

Hallo Leute,

ich fange gerade etwas neuer mit Python an und arbeite dazu gerade ein Buch zu Algorithmen durch. Dabei bin ich auf folgende Zeilen Code zum erzeugen von Fibonacci-Zahlen gestoßen gestoßen:

Code: Alles auswählen

from typing import Generator
def fib(n: int) -> Generator[int, None, None]:
	yield 0 #Spezialfall
	if n > 0: yield 1 #Spezialfall
	last: int = 0 #Am Anfang auf fib(0) setzen
	next: int = 1 #Am Anfagn auf fib(1) setzen
	for _ in range(1, n):
		last, next = next, last + next
		yield next #Haupt-Generatorschritt
Diesen Codeausschnitt habe ich auch Verstanden und wie die Sprünge über die yield-Anweisung funktionieren auch. Dazu kommt jetzt aber folgende main Funktion:

Code: Alles auswählen

if __name__ == "__main__"
	for i in fib(50):
	print(i)
Dabei verstehe ich den Aufruf in for i in fib(50) nicht so ganz. fib(50) ist in dem Fall ja keine range Angabe. Woher weiß das Programm dann wie oft die Schleife ausgeführt werden soll und irgendwie fehlt mir hier auch die next() Anweisung. Kann mir jemand diesen Zusammenhang eventuell erklären. Vielen dank schonmal :)

Mit freundlichen Grüßen
Inatrox
Benutzeravatar
noisefloor
User
Beiträge: 3858
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

die range-Angabe ist auch auch in der Generator-Funktion. Was du im `main` Aufruf machst ist einfach über den Generator iterieren. Die ganze Rechenlogik liegt ja _im_ Generator selber und mit der `for ... in ...` Schleife rufst du die Werte nacheinander ab.

Vereinfachtes Beispiel:

Code: Alles auswählen

>>> def simple_generator(count):
...     for i in range(count):
...         yield(i)
...
>>> for number in simple_generator(5):
...      print(number)
...
0
1
2
3
4
>>>
Gruß, noisefloor
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Inatrox: Die Funktion ist komisch bis fehlerhaft. Die beiden Spezialfälle sollte es nicht geben, denn egal womit man die Funktion aufruft, werden immer die erste Zahl geliefert. Das sollte nicht sein. Die Funktion ist dadurch auch unnötig ”speziell”, denn man kommt auch ohne die beiden extra ``yield`` aus.

Dann bin ich persönlich ja kein Fan von Typannotationen. Aber gut, wenn man die schon verwendet, dann bitte *sinnvoll* und bei so etwas wie ``last: int = 0`` ist die Typannotation so was von unsinnig. Jeder Mensch sieht das da eine ganze Zahl zugewiesen wird. Und auch die Werkzeuge zur Typprüfung sehen das. Python ist nicht Java. (Und selbst Java hat mittlerweile ``auto``.)

`Generator` als Rückgabetyp zu annotieren macht IMHO auch keinen Sinn wenn man da weder was rein-senden kann, noch ``return <irgendwas>`` benutzt wird. Denn dann verhält der sich wie ein Iterator und der lässt sich einfacher annotieren.

`last` ist sprachlich nicht so astrein, das sollte `previous` heissen. Und `next` muss anders heissen weil das schon der Name einer eingebauten Funktion ist.

Auch Blöcke die nur eine Zeile lang sind, sollten nicht direkt nach dem ":" stehen, sondern in einer eigenen Zeile.

Ungetestet:

Code: Alles auswählen

from typing import Iterator


def fib(n: int) -> Iterator[int]:
    current_number, next_number = 0, 1
    for _ in range(n):
        yield current_number
        current_number, next_number = next_number, current_number + next_number
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Inatrox
User
Beiträge: 6
Registriert: Montag 26. Juni 2023, 12:44

Danke für die Hilfe, ich habe es jetzt glaube verstanden. @__blackjack__: Die Funktion habe ich so eins zu eins aus dem Buch übernommen. Komme ursprünglich von C++ und fand das mit der Typannotation auch komisch. Werde das selbst auch nicht so machen. Die zwei yield-Anweisungen diesen meines Kenntnissstandes auch nur dazu um die ersten zwei Fibonacci-Zahlen immer mit auszugeben (0 und 1). Zudem fand ich die Variablennamen auch nicht unbedingt sinnvoll gewählt. Mit der Frage ging es mir ja eigentlich nur um die Logik hinter den yield Anweisungen.

Mfg
Inatrox
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Inatrox: Bei C++ sind die Typannotationen ja Deklarationen und a) tatsächlich notwendig, und haben b) einen realen Einfluss auf das Programm, also ob es kompiliert, und auch auf das Laufzeitverhalten. Das ist bei Python beides nicht der Fall, darum ist es IMHO so wichtig, dass man da auch tatsächlich regelmässig, am besten in den Editor oder die IDE integriert, auch prüft ob die Typannotationen korrekt sind.

Also wenn ich keinen Fehler gemacht habe, dann sollten bei meiner Variante auch immer 0 und 1 als erste Zahlen geliefert werden. Allerdings ist das aus dem Buch komisch und IMHO halt falsch das es immer n+1 Zahlen liefert und nicht n, und es ist deutlich umständlicher geschrieben als es sein müsste. Selbst wenn man tatsächlich eine API haben will wo man `n` übergibt und da immer n+1 Elemente generiert haben möchte.

An der Schnittstelle dieser Funktion mag ich aber auch nicht, dass die überhaupt ein `n` entgegen nimmt. Damit kann man die für keine Aufgabenstellung robust benutzen bei der man Fibonaccizahlen bis zu einer bestimmten Grösse generieren muss, oder wo die Abbruchbedingung von irgendeiner Verrechnung der generierten Werte abhängt. Eine allgemeine Funktion zum generieren der Fibonacci-Folge und dann konkret die ersten 50 davon ausgeben, hätte ich so geschrieben:

Code: Alles auswählen

#!/usr/bin/env python3
from itertools import islice


def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b


def main():
    for i in islice(fib(), 50):
        print(i)


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten