W tym epizodzie poznamy podstawy generatorów w Pythonie – jak je tworzyć, jak z nich korzystać, jak one działają. Napiszemy sobie 3 proste zadania, które – mam nadzieję – nieco rozjaśnią nam ten osobliwy temat, który trudno zrozumieć tak łatwo, jak zwykłe funkcje.

Generatory – yield, działanie

Generatory są łatwiejsze, niż się wydaje. Są to takie funkcje, które zamiast słówka return zwracają yield, które niejako pałzuje działanie funkcji, ale nie do końca.

Najlepiej to będzie pokazać na przykładzie:

def counter(max):
    current = 1
    while current <= max:
        yield current
        current += 1

Funkcja z 'return’ kończy się, gdy uderzymy w return statement i w zasadzie nie ma sensu nic dalej pisać, bo nic po returnie się nie wykona.

Generator zaś w momencie natrafienia na 'yield’ się wpada w pewną pauzę, wykonując jednocześnie dalszy element kodu. Aby generator się wyczerpał, muszą mu się skończyć 'yeildy’.

Zobaczmy, jak możemy tego użyć:

def counter(max):
    current = 1
    while current <= max:
        yield current
        current += 1

for num in counter(3):
    print(num)
#1
#2
#3

Aha, czyli wniosek z tego taki:

  • Zmienne wewnątrz funkcji counter zachowują swój stan, current się zwiększa o 1, nie ginie w pamięci
  • Coś, co jest napisane po 'yield’ wykonuje się
  • Generator wyczerpuje się wtedy, gdy nie ma już więcej możliwości wykonania żadnego 'yield’
  • Można przechodzić po generatorze w pętli for

Jeżeli nadal jeszcze nie do końca rozumiemy, to wydaje mi się, że najlepiej będzie po prostu dać kilka kolejnych przykładów.

Zadanie 1 – dni tygodnia

Mamy za zadanie stworzyć generator, który wypisuje dni tygodnia, od poniedziałku, do niedzieli, a później kończy.

Potrzebujemy funkcji, która zawiera listę dni tygodnia. Po tej liście przejdziemy sobie pętlą for, za każdym razem zwracając dzień przy pomocy słówka kluczowego yield:

def week():
    days = [
        "Monday",
        "Tuesday",
        "Wednesday",
        "Thursday",
        "Friday",
        "Saturday",
        "Sunday"
    ]
    for day in days:
        yield day

for d in week():
    print(d)

# Monday
# Tuesday
# Wednesday
# Thursday
# Friday
# Saturday
# Sunday

Nie ma w tym nic trudnego, może poza samą koncepcją funkcji, która jakoś „pamięta” swój tzw. stan.

Zadanie 2 – litery alfabetu

Piszemy generator, który wypisuje litery alfabetu, od 'a’ do 'z’, według ASCII. Aby się do tego zabrać, warto wiedzieć jaka liczba odpowiada literom a i z.

Nie musimy używać Google, możemy to zrobić w Pythonie:

print(ord('a'), ord('z'))
# 97 122

Możemy też użyć wielkich liter, one mają inne liczby:

print(ord('A'), ord('Z'))
# 65 90

Tak czy inaczej, jesteśmy w stanie stworzyć teraz listę liter od a do z:

letters = [chr(num) for num in range(97,123)]
print(letters)
#['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

Tak, 123 – range działa w ten sposób, że da nam liczby od 97 do 123 bez 123. Funkcja chr zamienia liczby na odpowiednie litery, przeciwieństwo ord.

Na upartego moglibyśmy to zrobić tak:

letters = [chr(num) for num in range(ord('a'),ord('z')+1)]
print(letters)
#['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

Teraz możemy napisać naszą funkcję z generatorem

def alphabet():
    al = [chr(n) for n in range(97,123)]
    for letter in al:
        yield letter


for let in alphabet():
    print(let)

Jak widać działa. Oczywiście możemy używać go w inny sposób:

def alphabet():
    al = [chr(n) for n in range(97,123)]
    for letter in al:
        yield letter


alph = alphabet()
print(next(alph))
# a
print(next(alph))
# b

Zadanie 3 – toggle on-off

Piszemy generator, który będzie działał w nieskończoność. Na przemian będzie nam podawał wartości on/off, zaczynając od on.

Do dzieła:

def toggle_state():
    state = "on"
    while True:
        yield state
        state = "off" if state == "on" else "off"

state = toggle_state()
print(next(state))
# on
print(next(state))
# off

Jak widać, stan początkowy to on. Pętla nieskończona – bo taki ma być generator, nie próbujmy go tylko potem w pętli for wywoływać, bo będzie to robić w nieskończoność.

Najpierw mamy yield, później zamiana z on na off. Warto zauważyć, że pierwsza linijka funkcji wykonuje się tylko raz i nie nadpisuje nam 'off’ na 'on’. Za drugim razem, gdy używamy generatora, zaczynamy wewnątrz pętli while.

Dziwny ternary operator w Pythonie nie musi się podobać, więc podam przykład na true/false:

def toggle_state():
    state = True
    while True:
        yield state
        state = not state

state = toggle_state()
print(next(state))
# True
print(next(state))
# False

Zaczynamy ze stanem ustawionym na True. Za pierwszym razem – zwracamy ten stan. Potem zamieniamy stan na jego logiczne przeciwieństwo.

Za drugim razem – zaczynamy w pętli while, zwracamy stan (fałsz) i zamieniamy na jego przeciwieństwo.

I można tak w nieskończoność, dosłownie.

Generatory mogą wydawać się dziwne, trudne, albo mało potrzebne, ale warto je znać. Ich znajomość może opłacić się w przyszłości, także przy czytaniu cudzego kodu.