Zaczynam uczyć się Pythona i natknąłem się na funkcje generatora, te, które zawierają w sobie instrukcję return. Chcę wiedzieć, jakie rodzaje problemów te funkcje są naprawdę dobre w rozwiązywaniu.
Zaczynam uczyć się Pythona i natknąłem się na funkcje generatora, te, które zawierają w sobie instrukcję return. Chcę wiedzieć, jakie rodzaje problemów te funkcje są naprawdę dobre w rozwiązywaniu.
Odpowiedzi:
Generatory dają leniwą ocenę. Używasz ich, iterując nad nimi, albo jawnie z „for”, albo pośrednio, przekazując je do dowolnej funkcji lub konstrukcji, która się iteruje. Możesz myśleć o generatorach jako o zwracaniu wielu elementów, tak jakby zwracały one listę, ale zamiast zwracać je wszystkie naraz, zwracają je jeden po drugim, a funkcja generatora jest wstrzymywana do momentu żądania następnego elementu.
Generatory są dobre do obliczania dużych zestawów wyników (w szczególności obliczeń obejmujących same pętle), w których nie wiesz, czy będziesz potrzebować wszystkich wyników lub gdzie nie chcesz przydzielić pamięci dla wszystkich wyników w tym samym czasie . Lub w sytuacjach, gdy generator korzysta z innego generatora lub zużywa inne zasoby, a wygodniej jest, jeśli dzieje się to tak późno, jak to możliwe.
Innym zastosowaniem generatorów (tak naprawdę jest to samo) jest zastąpienie wywołań zwrotnych iteracją. W niektórych sytuacjach chcesz, aby funkcja wykonała dużo pracy i od czasu do czasu zgłaszała się do osoby dzwoniącej. Tradycyjnie używałbyś do tego funkcji wywołania zwrotnego. Przekazujesz to wywołanie zwrotne do funkcji pracy i okresowo wywołuje to wywołanie zwrotne. Podejście oparte na generatorze polega na tym, że funkcja pracy (obecnie generator) nie wie nic o wywołaniu zwrotnym i po prostu ustępuje, gdy chce coś zgłosić. Program wywołujący, zamiast pisać osobne wywołanie zwrotne i przekazywać je do funkcji pracy, wykonuje wszystkie czynności raportowania w małej pętli „za” wokół generatora.
Załóżmy na przykład, że napisałeś program „wyszukiwania systemu plików”. Możesz przeprowadzić wyszukiwanie w całości, zebrać wyniki, a następnie wyświetlić je pojedynczo. Wszystkie wyniki musiałyby zostać zebrane przed pokazaniem pierwszego, a wszystkie wyniki byłyby w tym samym czasie w pamięci. Możesz też wyświetlać wyniki podczas ich wyszukiwania, co byłoby bardziej wydajne pod względem pamięci i bardziej przyjazne dla użytkownika. To ostatnie można wykonać, przekazując funkcję drukowania wyników do funkcji wyszukiwania systemu plików, lub można to zrobić, po prostu czyniąc funkcję wyszukiwania generatorem i iterując wynik.
Jeśli chcesz zobaczyć przykład dwóch ostatnich podejść, zobacz os.path.walk () (stara funkcja chodzenia po systemie plików z wywołaniem zwrotnym) i os.walk () (nowy generator chodzenia po systemie plików). Oczywiście, jeśli naprawdę chciałeś zebrać wszystkie wyniki na liście, podejście generatora jest trywialne, aby przekonwertować je na podejście z dużą listą:
big_list = list(the_generator)
yield
i join
po nich, aby uzyskać następny wynik, nie jest on wykonywany równolegle (i nie robi tego żaden standardowy generator bibliotek; potajemnie uruchamiane wątki są odrzucone). Generator zatrzymuje się przy każdym, yield
aż zostanie zażądana następna wartość. Jeśli generator pakuje operacje we / wy, system operacyjny może proaktywnie buforować dane z pliku przy założeniu, że zostanie wkrótce o to poproszony, ale taki jest system operacyjny, w którym nie jest zaangażowany Python.
Jednym z powodów użycia generatora jest uczynienie rozwiązania bardziej zrozumiałym dla niektórych rozwiązań.
Drugi polega na traktowaniu wyników pojedynczo, unikając tworzenia ogromnych list wyników, które i tak byłyby przetwarzane oddzielnie.
Jeśli masz funkcję Fibonacciego-up-to-n:
# function version
def fibon(n):
a = b = 1
result = []
for i in xrange(n):
result.append(a)
a, b = b, a + b
return result
Możesz łatwiej napisać funkcję w ten sposób:
# generator version
def fibon(n):
a = b = 1
for i in xrange(n):
yield a
a, b = b, a + b
Funkcja jest bardziej przejrzysta. A jeśli użyjesz takiej funkcji:
for x in fibon(1000000):
print x,
w tym przykładzie, jeśli używasz wersji generatora, cała lista 1000000 pozycji nie zostanie utworzona, tylko jedna wartość na raz. Nie byłoby tak w przypadku korzystania z wersji listy, w której lista byłaby tworzona jako pierwsza.
list(fibon(5))
Zobacz sekcję „Motywacja” w PEP 255 .
Nieoczywistym zastosowaniem generatorów jest tworzenie funkcji przerywalnych, które umożliwiają wykonywanie takich czynności, jak aktualizacja interfejsu użytkownika lub uruchamianie kilku zadań „jednocześnie” (w rzeczywistości z przeplotem), bez korzystania z wątków.
Znajduję to wyjaśnienie, które rozwiewa moje wątpliwości. Ponieważ istnieje możliwość, że osoba, która nie wie, Generators
również nie wie o tymyield
Powrót
Instrukcja return to miejsce, w którym wszystkie zmienne lokalne są niszczone, a wynikowa wartość jest zwracana (zwracana) programowi wywołującemu. Jeśli jakiś czas później zostanie wywołana ta sama funkcja, funkcja otrzyma nowy zestaw zmiennych.
Wydajność
Ale co, jeśli zmienne lokalne nie zostaną wyrzucone po wyjściu z funkcji? Oznacza to, że możemy resume the function
tam, gdzie przerwaliśmy. W tym miejscu wprowadzana jest koncepcja, generators
a yield
stwierdzenie zostaje wznowione tam, gdzie function
zostało przerwane.
def generate_integers(N):
for i in xrange(N):
yield i
In [1]: gen = generate_integers(3)
In [2]: gen
<generator object at 0x8117f90>
In [3]: gen.next()
0
In [4]: gen.next()
1
In [5]: gen.next()
Na tym polega różnica między return
i yield
instrukcjami w Pythonie.
Instrukcja Yield sprawia, że funkcja jest funkcją generatora.
Generatory są więc prostym i wydajnym narzędziem do tworzenia iteratorów. Są napisane jak zwykłe funkcje, ale używają yield
instrukcji, ilekroć chcą zwrócić dane. Za każdym razem, gdy wywoływana jest funkcja next (), generator wznawia pracę od miejsca, w którym został przerwany (zapamiętuje wszystkie wartości danych i ostatnią instrukcję).
Załóżmy, że masz 100 milionów domen w tabeli MySQL i chcesz zaktualizować pozycję Alexa dla każdej domeny.
Pierwszą rzeczą, której potrzebujesz, jest wybranie nazw domen z bazy danych.
Powiedzmy, że twoja nazwa tabeli to domains
i nazwa kolumny domain
.
Jeśli użyjesz SELECT domain FROM domains
, zwróci 100 milionów wierszy, co zużyje dużo pamięci. Twój serwer może ulec awarii.
Zdecydowałeś się więc uruchomić program partiami. Powiedzmy, że nasz rozmiar partii to 1000.
W naszej pierwszej partii sprawdzimy pierwsze 1000 wierszy, sprawdzimy pozycję Alexa dla każdej domeny i zaktualizujemy wiersz bazy danych.
W naszej drugiej partii będziemy pracować nad następnymi 1000 rzędami. W naszej trzeciej partii będzie to od 2001 do 3000 i tak dalej.
Teraz potrzebujemy funkcji generatora, która generuje nasze partie.
Oto nasza funkcja generatora:
def ResultGenerator(cursor, batchsize=1000):
while True:
results = cursor.fetchmany(batchsize)
if not results:
break
for result in results:
yield result
Jak widać, nasza funkcja stale zapisuje yield
wyniki. Jeśli użyjesz słowa kluczowego return
zamiast yield
, cała funkcja zostanie zakończona, gdy osiągnie wartość return.
return - returns only once
yield - returns multiple times
Jeśli funkcja używa słowa kluczowego, yield
to jest to generator.
Teraz możesz iterować w ten sposób:
db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
doSomethingWith(result)
db.close()
Buforowanie Gdy efektywne jest pobieranie danych w dużych porcjach, ale przetwarzanie ich w małych porcjach, generator może pomóc:
def bufferedFetch():
while True:
buffer = getBigChunkOfData()
# insert some code to break on 'end of data'
for i in buffer:
yield i
Powyższe pozwala łatwo oddzielić buforowanie od przetwarzania. Funkcja konsumenta może teraz tylko pobierać wartości jeden po drugim, nie martwiąc się o buforowanie.
Przekonałem się, że generatory są bardzo pomocne w czyszczeniu twojego kodu i dają ci bardzo unikalny sposób kapsułkowania i modularyzacji kodu. W sytuacji, gdzie trzeba coś stale wypluć wartości na podstawie własnego wewnętrznego przetwarzania A kiedy czegoś potrzebuje być wywoływana z dowolnego miejsca w kodzie (i nie tylko wewnątrz pętli lub bloku na przykład), generatory są cechą posługiwać się.
Abstrakcyjnym przykładem może być generator liczb Fibonacciego, który nie żyje w pętli, a gdy zostanie wywołany z dowolnego miejsca, zawsze zwróci następną liczbę w sekwencji:
def fib():
first = 0
second = 1
yield first
yield second
while 1:
next = first + second
yield next
first = second
second = next
fibgen1 = fib()
fibgen2 = fib()
Teraz masz dwa obiekty generatora liczb Fibonacciego, które możesz wywoływać z dowolnego miejsca w kodzie i zawsze będą zwracać coraz większe liczby Fibonacciego w następujący sposób:
>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5
Zaletą generatorów jest to, że otaczają one stan bez konieczności przechodzenia przez obręcz tworzenia obiektów. Jednym ze sposobów myślenia o nich są „funkcje”, które zapamiętują ich stan wewnętrzny.
Mam przykład Fibonacciego z Python Generators - czym one są? a przy odrobinie wyobraźni możesz wymyślić wiele innych sytuacji, w których generatory stanowią doskonałą alternatywę dla for
pętli i innych tradycyjnych konstrukcji iteracyjnych.
Proste wyjaśnienie: rozważ for
oświadczenie
for item in iterable:
do_stuff()
Przez większość czasu wszystkie elementy iterable
nie muszą być od samego początku, ale można je generować w locie, gdy są potrzebne. W obu przypadkach może to być znacznie bardziej wydajne
Innym razem nie znasz nawet wszystkich przedmiotów przed czasem. Na przykład:
for command in user_input():
do_stuff_with(command)
Nie masz możliwości wcześniejszego poznania wszystkich poleceń użytkownika, ale możesz skorzystać z takiej ładnej pętli, jeśli generator generuje polecenia:
def user_input():
while True:
wait_for_command()
cmd = get_command()
yield cmd
Za pomocą generatorów można także wykonywać iteracje po nieskończonych sekwencjach, co oczywiście nie jest możliwe podczas iteracji po kontenerach.
itertool
- patrz cycles
.
Moje ulubione zastosowania to operacje „filtrowania” i „zmniejszania”.
Powiedzmy, że czytamy plik i chcemy tylko linii zaczynających się od „##”.
def filter2sharps( aSequence ):
for l in aSequence:
if l.startswith("##"):
yield l
Następnie możemy użyć funkcji generatora w odpowiedniej pętli
source= file( ... )
for line in filter2sharps( source.readlines() ):
print line
source.close()
Przykład redukcji jest podobny. Załóżmy, że mamy plik, w którym musimy zlokalizować bloki <Location>...</Location>
linii. [Nie tagi HTML, ale linie wyglądające jak tagi].
def reduceLocation( aSequence ):
keep= False
block= None
for line in aSequence:
if line.startswith("</Location"):
block.append( line )
yield block
block= None
keep= False
elif line.startsWith("<Location"):
block= [ line ]
keep= True
elif keep:
block.append( line )
else:
pass
if block is not None:
yield block # A partial block, icky
Ponownie możemy użyć tego generatora we właściwej pętli for.
source = file( ... )
for b in reduceLocation( source.readlines() ):
print b
source.close()
Chodzi o to, że funkcja generatora pozwala nam filtrować lub redukować sekwencję, tworząc kolejną sekwencję po jednej wartości na raz.
fileobj.readlines()
czytałby cały plik do listy w pamięci, co przeczy celowi używania generatorów. Ponieważ obiekty plików są już iterowalne, możesz for b in your_generator(fileobject):
zamiast tego użyć . W ten sposób plik będzie odczytywany jeden wiersz na raz, aby uniknąć odczytu całego pliku.
Praktycznym przykładem, w którym można skorzystać z generatora jest, jeśli masz jakiś kształt i chcesz iterować po jego rogach, krawędziach itp. Do własnego projektu ( tutaj kod źródłowy ) miałem prostokąt:
class Rect():
def __init__(self, x, y, width, height):
self.l_top = (x, y)
self.r_top = (x+width, y)
self.r_bot = (x+width, y+height)
self.l_bot = (x, y+height)
def __iter__(self):
yield self.l_top
yield self.r_top
yield self.r_bot
yield self.l_bot
Teraz mogę utworzyć prostokąt i zapętlić jego rogi:
myrect=Rect(50, 50, 100, 100)
for corner in myrect:
print(corner)
Zamiast tego __iter__
możesz mieć metodę iter_corners
i wywołać ją za pomocą for corner in myrect.iter_corners()
. Jest po prostu bardziej elegancki w użyciu, __iter__
ponieważ odtąd możemy używać nazwy instancji klasy bezpośrednio w for
wyrażeniu.
Kilka dobrych odpowiedzi tutaj, ale polecam również pełną lekturę samouczka programowania funkcjonalnego Python, który pomaga wyjaśnić niektóre z bardziej potencjalnych przypadków użycia generatorów.
Ponieważ nie wspomniano o metodzie wysyłania generatora, oto przykład:
def test():
for i in xrange(5):
val = yield
print(val)
t = test()
# Proceed to 'yield' statement
next(t)
# Send value to yield
t.send(1)
t.send('2')
t.send([3])
Pokazuje możliwość wysłania wartości do działającego generatora. Bardziej zaawansowany kurs na temat generatorów w poniższym filmie (w tym yield
z eksploracji, generatorów do przetwarzania równoległego, przekraczania limitu rekurencji itp.)
Stosy rzeczy. Za każdym razem, gdy chcesz wygenerować sekwencję elementów, ale nie chcesz „zmaterializować” ich wszystkich na liście jednocześnie. Na przykład możesz mieć prosty generator, który zwraca liczby pierwsze:
def primes():
primes_found = set()
primes_found.add(2)
yield 2
for i in itertools.count(1):
candidate = i * 2 + 1
if not all(candidate % prime for prime in primes_found):
primes_found.add(candidate)
yield candidate
Następnie możesz użyć tego do wygenerowania produktów kolejnych liczb pierwszych:
def prime_products():
primeiter = primes()
prev = primeiter.next()
for prime in primeiter:
yield prime * prev
prev = prime
Są to dość trywialne przykłady, ale możesz zobaczyć, jak może być przydatne do przetwarzania dużych (potencjalnie nieskończonych!) Zestawów danych bez generowania ich z wyprzedzeniem, co jest tylko jednym z bardziej oczywistych zastosowań.
Nadaje się również do drukowania liczb pierwszych do n:
def genprime(n=10):
for num in range(3, n+1):
for factor in range(2, num):
if num%factor == 0:
break
else:
yield(num)
for prime_num in genprime(100):
print(prime_num)