Szukaj…


Wprowadzenie

Generatory są leniwymi iteratorami utworzonymi przez funkcje generatora (przy użyciu yield ) lub wyrażenia generatora (przy użyciu (an_expression for x in an_iterator) ).

Składnia

  • wydaj <expr>
  • wydajność z <expr>
  • <var> = wydajność <expr>
  • dalej ( <iter> )

Iteracja

Obiekt generatora obsługuje protokół iteratora . Oznacza to, że udostępnia metodę next() ( __next__() w Pythonie 3.x), która jest używana do przechodzenia przez jej wykonanie, a jej metoda __iter__ zwraca się. Oznacza to, że generator może być używany w dowolnej konstrukcji języka obsługującej ogólne obiekty iterowalne.

# naive partial implementation of the Python 2.x xrange()
def xrange(n):
    i = 0
    while i < n:
        yield i
        i += 1

# looping
for i in xrange(10):
    print(i)  # prints the values 0, 1, ..., 9

# unpacking
a, b, c = xrange(3)  # 0, 1, 2

# building a list
l = list(xrange(10))  # [0, 1, ..., 9]

Następna funkcja ()

next() wbudowane next() jest wygodne opakowanie, którego można użyć do otrzymania wartości z dowolnego iteratora (w tym iteratora generatora) i do zapewnienia wartości domyślnej w przypadku wyczerpania iteratora.

def nums():
    yield 1
    yield 2
    yield 3
generator = nums()

next(generator, None)  # 1
next(generator, None)  # 2
next(generator, None)  # 3
next(generator, None)  # None
next(generator, None)  # None
# ...

next(iterator[, default]) jest składnia next(iterator[, default]) . Jeśli iterator kończy się i wartość domyślna została przekazana, jest zwracana. Jeśli nie podano wartości domyślnej, StopIteration jest podnoszony.

Wysyłanie obiektów do generatora

Oprócz otrzymywania wartości z generatora możliwe jest wysyłanie obiektu do generatora za pomocą metody send() .

def accumulator():
    total = 0
    value = None
    while True:
        # receive sent value
        value = yield total
        if value is None: break
        # aggregate values
        total += value

generator = accumulator()

# advance until the first "yield"
next(generator)      # 0

# from this point on, the generator aggregates values
generator.send(1)    # 1
generator.send(10)   # 11
generator.send(100)  # 111
# ...

# Calling next(generator) is equivalent to calling generator.send(None)
next(generator)      # StopIteration

Oto, co się tutaj dzieje:

  • Kiedy po raz pierwszy wywołujesz next(generator) , program przechodzi do pierwszej instrukcji yield i zwraca wartość total w tym punkcie, która wynosi 0. Wykonanie generatora zawiesza się w tym momencie.
  • Kiedy wywołujesz generator.send(x) , interpreter przyjmuje argument x i czyni z niego wartość zwracaną z ostatniej instrukcji yield , która zostaje przypisana do value . Generator następnie postępuje jak zwykle, dopóki nie uzyska następnej wartości.
  • Kiedy w końcu wywołujesz next(generator) , program traktuje to tak, jakbyś wysyłał None do generatora. Nie ma nic specjalnego w None , jednak w tym przykładzie użyto None jako specjalnej wartości, aby poprosić generator o zatrzymanie.

Wyrażenia generatora

Możliwe jest tworzenie iteratorów generatorów przy użyciu składni podobnej do zrozumienia.

generator = (i * 2 for i in range(3))

next(generator)  # 0
next(generator)  # 2
next(generator)  # 4
next(generator)  # raises StopIteration

Jeśli funkcja niekoniecznie musi zostać przekazana do listy, możesz zaoszczędzić na znakach (i poprawić czytelność) poprzez umieszczenie wyrażenia generatora w wywołaniu funkcji. Nawiasy z wywołania funkcji domyślnie sprawiają, że wyrażenie jest wyrażeniem generującym.

sum(i ** 2 for i in range(4))  # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14

Dodatkowo zaoszczędzisz na pamięci, ponieważ zamiast ładowania całej listy, nad którą wykonujesz iterację ( [0, 1, 2, 3] w powyższym przykładzie), generator pozwala Pythonowi na użycie wartości w razie potrzeby.

Wprowadzenie

Wyrażenia generatora są podobne do list, słowników i zestawów, ale są zamknięte w nawiasach. Nawiasy nie muszą być obecne, gdy są używane jako jedyny argument wywołania funkcji.

expression = (x**2 for x in range(10))

Ten przykład generuje 10 pierwszych idealnych kwadratów, w tym 0 (w których x = 0).

Funkcje generatora są podobne do funkcji zwykłych, z tym wyjątkiem, że mają jedno lub więcej yield w ciele. Takie funkcje nie mogą return żadnych wartości (jednak puste return s są dozwolone, jeśli chcesz wcześniej zatrzymać generator).

def function():
    for x in range(10):
        yield x**2

Ta funkcja generatora jest równoważna z poprzednim wyrażeniem generatora, generuje to samo.

Uwaga : wszystkie wyrażenia generatora mają swoje równoważne funkcje, ale nie odwrotnie.


Wyrażenie generujące może być użyte bez nawiasów, jeśli oba nawiasy byłyby powtórzone w przeciwnym razie:

sum(i for i in range(10) if i % 2 == 0)   #Output: 20
any(x = 0 for x in foo)                   #Output: True or False depending on foo
type(a > b for a in foo if a % 2 == 1)    #Output: <class 'generator'>

Zamiast:

sum((i for i in range(10) if i % 2 == 0))
any((x = 0 for x in foo))
type((a > b for a in foo if a % 2 == 1))

Ale nie:

fooFunction(i for i in range(10) if i % 2 == 0,foo,bar)
return x = 0 for x in foo
barFunction(baz, a > b for a in foo if a % 2 == 1)

Wywołanie funkcji generatora powoduje wygenerowanie obiektu generatora , który można później iterować. W przeciwieństwie do innych typów iteratorów, obiekty generatora można przemierzać tylko raz.

g1 = function()
print(g1)  # Out: <generator object function at 0x1012e1888>

Zauważ, że ciało generatora nie jest natychmiast wykonywane: gdy wywołujesz function() w powyższym przykładzie, natychmiast zwraca obiekt generatora, bez wykonywania nawet pierwszej instrukcji print. Pozwala to generatorom zużywać mniej pamięci niż funkcje zwracające listę oraz umożliwia tworzenie generatorów, które wytwarzają nieskończenie długie sekwencje.

Z tego powodu generatory są często używane w analizie danych i innych kontekstach zawierających duże ilości danych. Kolejną zaletą jest to, że inny kod może natychmiast użyć wartości wygenerowanych przez generator, bez czekania na wygenerowanie pełnej sekwencji.

Jeśli jednak trzeba użyć wartości wygenerowanych przez generator więcej niż jeden raz, a wygenerowanie ich kosztuje więcej niż przechowywanie, lepiej przechowywać przechowywane wartości jako list niż ponownie wygenerować sekwencję. Aby uzyskać więcej informacji, zobacz „Resetowanie generatora” poniżej.

Zazwyczaj obiekt generatora jest używany w pętli lub w dowolnej funkcji wymagającej iteracji:

for x in g1:
    print("Received", x)

# Output:
# Received 0
# Received 1
# Received 4
# Received 9
# Received 16
# Received 25
# Received 36
# Received 49
# Received 64
# Received 81

arr1 = list(g1)
# arr1 = [], because the loop above already consumed all the values.
g2 = function()
arr2 = list(g2)  # arr2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Ponieważ obiekty generatora są iteratorami, można iterować je ręcznie za pomocą funkcji next() . Spowoduje to zwrócenie uzyskanych wartości jeden po drugim przy każdym kolejnym wywołaniu.

Pod maską za każdym razem, gdy wywołujesz funkcję next() w generatorze, Python wykonuje instrukcje w treści funkcji generatora, dopóki nie trafi ona do następnej instrukcji yield . W tym momencie zwraca argument polecenia yield i pamięta punkt, w którym to się wydarzyło. Ponowne wywołanie next() wznowi wykonywanie od tego momentu i będzie kontynuowane aż do następnej instrukcji yield .

Jeśli Python osiągnie koniec funkcji generatora, nie napotkając żadnej yield , powstaje wyjątek StopIteration (jest to normalne, wszystkie iteratory zachowują się w ten sam sposób).

g3 = function()
a = next(g3)  # a becomes 0
b = next(g3)  # b becomes 1
c = next(g3)  # c becomes 2
...
j = next(g3)  # Raises StopIteration, j remains undefined

Zauważ, że w Pythonie 2 obiekty generatora miały metody .next() , których można użyć do ręcznego iterowania uzyskanych wartości. W Pythonie 3 ta metoda została zastąpiona standardem .__next__() dla wszystkich iteratorów.

Resetowanie generatora

Należy pamiętać, że można tylko iterację obiektów generowanych przez generator raz. Jeśli wykonałeś już iterację po obiektach w skrypcie, każda kolejna próba spowoduje None .

Jeśli potrzebujesz użyć obiektów wygenerowanych przez generator więcej niż jeden raz, możesz albo ponownie zdefiniować funkcję generatora i użyć jej drugi raz, albo alternatywnie możesz zapisać dane wyjściowe funkcji generatora na liście przy pierwszym użyciu. Ponowne zdefiniowanie funkcji generatora będzie dobrym rozwiązaniem, jeśli masz do czynienia z dużymi ilościami danych, a przechowywanie listy wszystkich elementów danych zajęłoby dużo miejsca na dysku. I odwrotnie, jeśli początkowe generowanie elementów jest kosztowne, możesz chcieć przechowywać wygenerowane elementy na liście, abyś mógł z nich ponownie korzystać.

Używanie generatora do znajdowania liczb Fibonacciego

Praktycznym przykładem zastosowania generatora jest iteracja wartości nieskończonej serii. Oto przykład znalezienia pierwszych dziesięciu terminów sekwencji Fibonacciego .

def fib(a=0, b=1):
    """Generator that yields Fibonacci numbers. `a` and `b` are the seed values"""
    while True:
        yield a
        a, b = b, a + b

f = fib()
print(', '.join(str(next(f)) for _ in range(10)))

0, 1, 1, 2, 3, 5, 8, 13, 21, 34

Nieskończone sekwencje

Generatory mogą być używane do reprezentowania nieskończonych sekwencji:

def integers_starting_from(n):
    while True:
        yield n
        n += 1

natural_numbers = integers_starting_from(1)

Nieskończona sekwencja liczb jak wyżej może być również wygenerowana przy pomocy itertools.count . Powyższy kod można zapisać jak poniżej

natural_numbers = itertools.count(1)

Możesz używać opisów generatorów w generatorach nieskończonych do tworzenia nowych generatorów:

multiples_of_two = (x * 2 for x in natural_numbers)
multiples_of_three = (x for x in natural_numbers if x % 3 == 0)

Pamiętaj, że nieskończony generator nie ma końca, więc przekazanie go do dowolnej funkcji, która będzie próbowała całkowicie zużyć generator, będzie miał tragiczne konsekwencje :

list(multiples_of_two)  # will never terminate, or raise an OS-specific error

Zamiast tego użyj xrange list / set o range (lub xrange dla python <3.0):

first_five_multiples_of_three = [next(multiples_of_three) for _ in range(5)] 
# [3, 6, 9, 12, 15]

lub użyj itertools.islice() aby pokroić iterator na podzbiór:

from itertools import islice
multiples_of_four = (x * 4 for x in integers_starting_from(1))
first_five_multiples_of_four = list(islice(multiples_of_four, 5))
# [4, 8, 12, 16, 20]

Zauważ, że oryginalny generator również został zaktualizowany, podobnie jak wszystkie inne generatory pochodzące z tego samego „katalogu głównego”:

next(natural_numbers)    # yields 16
next(multiples_of_two)   # yields 34
next(multiples_of_four)  # yields 24

Nieskończoną sekwencję można również iterować za pomocą pętli for . Pamiętaj o dołączeniu warunkowej instrukcji break , aby pętla ostatecznie się zakończyła:

for idx, number in enumerate(multiplies_of_two):
    print(number)
    if idx == 9:
        break  # stop after taking the first 10 multiplies of two

Klasyczny przykład - liczby Fibonacciego

import itertools

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

first_ten_fibs = list(itertools.islice(fibonacci(), 10))
# [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

def nth_fib(n):
    return next(itertools.islice(fibonacci(), n - 1, n))

ninety_nineth_fib = nth_fib(99)  # 354224848179261915075

Uzyskanie wszystkich wartości z innej iterowalnej

Python 3.x 3.3

Użyj yield from jeśli chcesz uzyskać wszystkie wartości z innej iterowalnej:

def foob(x):
    yield from range(x * 2)
    yield from range(2)

list(foob(5))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1]

Działa to również z generatorami.

def fibto(n):
    a, b = 1, 1
    while True:
        if a >= n: break
        yield a
        a, b = b, a + b

def usefib():
    yield from fibto(10)
    yield from fibto(20)

list(usefib())  # [1, 1, 2, 3, 5, 8, 1, 1, 2, 3, 5, 8, 13]

Coroutines

Generatory mogą być używane do implementacji coroutines:

# create and advance generator to the first yield
def coroutine(func):
    def start(*args,**kwargs):
        cr = func(*args,**kwargs)
        next(cr)
        return cr
    return start

# example coroutine
@coroutine
def adder(sum = 0):
    while True:
        x = yield sum
        sum += x

# example use
s = adder()
s.send(1) # 1
s.send(2) # 3

Coroutyny są powszechnie używane do implementacji automatów stanów, ponieważ są one przede wszystkim przydatne do tworzenia procedur opartych na jednej metodzie, które wymagają prawidłowego funkcjonowania stanu. Działają w stanie istniejącym i zwracają wartość uzyskaną po zakończeniu operacji.

Wydajność z rekurencją: rekursywnie wyświetla listę wszystkich plików w katalogu

Najpierw zaimportuj biblioteki, które działają z plikami:

from os import listdir
from os.path import isfile, join, exists

Funkcja pomocnicza do odczytu tylko plików z katalogu:

def get_files(path):
    for file in listdir(path):
        full_path = join(path, file)
        if isfile(full_path):
            if exists(full_path):
                yield full_path

Kolejna funkcja pomocnicza, która pobiera tylko podkatalogi:

def get_directories(path):
    for directory in listdir(path):
        full_path = join(path, directory)
        if not isfile(full_path):
            if exists(full_path):
                yield full_path

Teraz użyj tych funkcji, aby rekurencyjnie uzyskać wszystkie pliki w katalogu i wszystkich jego podkatalogach (przy użyciu generatorów):

def get_files_recursive(directory):
    for file in get_files(directory):
        yield file
    for subdirectory in get_directories(directory):
        for file in get_files_recursive(subdirectory): # here the recursive call
            yield file

Funkcję tę można uprościć, stosując yield from :

def get_files_recursive(directory):
    yield from get_files(directory)
    for subdirectory in get_directories(directory):
        yield from get_files_recursive(subdirectory)

Iteracja nad generatorami równolegle

Aby iterować równolegle kilka generatorów, użyj wbudowanego zip :

for x, y in zip(a,b):
    print(x,y)

Prowadzi do:

1 x
2 y
3 z

W python 2 powinieneś użyć itertools.izip . Tutaj możemy również zobaczyć, że wszystkie funkcje zip dają krotki.

Pamiętaj, że zip przestanie iterować, gdy tylko jeden z elementów iteracyjnych skończy się z elementami. Jeśli chcesz iterować tak długo, jak najdłużej iterowalny, użyj itertools.zip_longest() .

Refaktoryzacja kodu budowania listy

Załóżmy, że masz złożony kod, który tworzy i zwraca listę, zaczynając od pustej listy i wielokrotnie do niej dołączając:

def create():
    result = []
    # logic here...
    result.append(value) # possibly in several places
    # more logic...
    return result # possibly in several places

values = create()

Jeśli zastąpienie wewnętrznej logiki zrozumieniem listy nie jest praktyczne, możesz zamienić całą funkcję w generator w miejscu, a następnie zebrać wyniki:

def create_gen():
    # logic...
    yield value
    # more logic
    return # not needed if at the end of the function, of course

values = list(create_gen())

Jeśli logika jest rekurencyjna, należy użyć yield from aby uwzględnić wszystkie wartości wywołania rekurencyjnego w wyniku „spłaszczonym”:

def preorder_traversal(node):
    yield node.value
    for child in node.children:
        yield from preorder_traversal(child)

Badawczy

next funkcja jest przydatna nawet bez iteracji. Przekazanie wyrażenia generatora do next jest szybkim sposobem na znalezienie pierwszego wystąpienia elementu pasującego do jakiegoś predykatu. Kod proceduralny jak

def find_and_transform(sequence, predicate, func):
    for element in sequence:
        if predicate(element):
            return func(element)
    raise ValueError

item = find_and_transform(my_sequence, my_predicate, my_func)

można zastąpić:

item = next(my_func(x) for x in my_sequence if my_predicate(x))
# StopIteration will be raised if there are no matches; this exception can
# be caught and transformed, if desired.

W tym celu może być pożądane utworzenie aliasu, takiego jak first = next lub funkcji otoki, aby przekonwertować wyjątek:

def first(generator):
    try:
        return next(generator)
    except StopIteration:
        raise ValueError


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow