Python Language
Generatory
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 instrukcjiyield
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 argumentx
i czyni z niego wartość zwracaną z ostatniej instrukcjiyield
, która zostaje przypisana dovalue
. 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 wNone
, jednak w tym przykładzie użytoNone
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
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