Czy posiadanie funkcji języka generatora, takiego jak yield
dobry pomysł?
Chciałbym odpowiedzieć na to pytanie z perspektywy Pythona, zdecydowanie , to świetny pomysł .
Zacznę od omówienia kilku pytań i założeń w twoim pytaniu, a następnie wykażę wszechobecność generatorów i ich nieuzasadnioną przydatność w Pythonie później.
Za pomocą zwykłej funkcji innej niż generator można ją wywołać, a jeśli otrzyma to samo wejście, zwróci to samo wyjście. Z wydajnością zwraca inną moc wyjściową, w zależności od jej stanu wewnętrznego.
To nieprawda. Metody na obiektach można traktować jako same funkcje z własnym stanem wewnętrznym. W Pythonie, ponieważ wszystko jest obiektem, możesz faktycznie pobrać metodę z obiektu i ominąć tę metodę (która jest powiązana z obiektem, z którego pochodzi, więc pamięta swój stan).
Inne przykłady obejmują celowo losowe funkcje, a także metody wprowadzania danych, takie jak sieć, system plików i terminal.
Jak taka funkcja pasuje do paradygmatu językowego?
Jeśli paradygmat języka obsługuje takie funkcje, jak funkcje pierwszej klasy, a generatory obsługują inne funkcje języka, takie jak protokół Iterable, to bez problemu się dopasowują.
Czy to faktycznie łamie jakieś konwencje?
Nie. Ponieważ jest on upieczony w języku, konwencje są zbudowane wokół i obejmują (lub wymagają!) Korzystanie z generatorów.
Czy kompilatory / tłumacze języka programowania muszą zerwać z konwencjami, aby wdrożyć taką funkcję
Podobnie jak w przypadku każdej innej funkcji, kompilator musi być po prostu zaprojektowany do obsługi tej funkcji. W przypadku Pythona funkcje są już obiektami ze stanem (takie jak domyślne argumenty i adnotacje funkcji).
czy język musi implementować wielowątkowość, aby ta funkcja działała, czy może to zrobić bez technologii wątkowania?
Ciekawostka: domyślna implementacja Pythona w ogóle nie obsługuje wątków. Posiada globalną blokadę interpretera (GIL), więc nic nie działa równolegle, chyba że uruchomisz drugi proces, aby uruchomić inną instancję Pythona.
Uwaga: przykłady znajdują się w Pythonie 3
Ponad wydajność
Chociaż yield
słowa kluczowego można użyć w dowolnej funkcji, aby zamienić go w generator, nie jest to jedyny sposób, aby go utworzyć. Python oferuje Generatory Expressions, potężny sposób na wyraźne wyrażenie generatora w kategoriach innego iterowalnego (w tym innych generatorów)
>>> pairs = ((x,y) for x in range(10) for y in range(10) if y >= x)
>>> pairs
<generator object <genexpr> at 0x0311DC90>
>>> sum(x*y for x,y in pairs)
1155
Jak widać, składnia jest nie tylko czysta i czytelna, ale także wbudowane funkcje, takie jak sum
generatory akceptacji.
Z
Sprawdź propozycję rozszerzenia Python dla instrukcji With . Jest bardzo różny, niż można się spodziewać po stwierdzeniu With w innych językach. Przy niewielkiej pomocy ze standardowej biblioteki generatory Pythona działają pięknie jako menedżery kontekstów.
>>> from contextlib import contextmanager
>>> @contextmanager
def debugWith(arg):
print("preprocessing", arg)
yield arg
print("postprocessing", arg)
>>> with debugWith("foobar") as s:
print(s[::-1])
preprocessing foobar
raboof
postprocessing foobar
Oczywiście drukowanie rzeczy jest najbardziej nudną rzeczą, jaką możesz tutaj zrobić, ale pokazuje widoczne rezultaty. Bardziej interesujące opcje obejmują automatyczne zarządzanie zasobami (otwieranie i zamykanie plików / strumieni / połączeń sieciowych), blokowanie współbieżności, tymczasowe zawijanie lub zastępowanie funkcji oraz dekompresowanie, a następnie ponowne kompresowanie danych. Jeśli wywoływanie funkcji jest jak wstrzykiwanie kodu do kodu, wówczas z instrukcjami jest jak zawijanie części kodu w inny kod. Niezależnie od tego, jak go używasz, jest to solidny przykład łatwego przechwytywania struktury języka. Generatory oparte na wydajności nie są jedynym sposobem tworzenia menedżerów kontekstu, ale z pewnością są wygodne.
Częściowe wyczerpanie
Pętle w Pythonie działają w ciekawy sposób. Mają następujący format:
for <name> in <iterable>:
...
Po pierwsze, wywołane <iterable>
przeze mnie wyrażenie jest oceniane w celu uzyskania iterowalnego obiektu. Po drugie, iterable go wywołało __iter__
, a wynikowy iterator jest przechowywany za kulisami. Następnie __next__
wywoływany jest iterator w celu uzyskania wartości powiązania z wprowadzoną nazwą <name>
. Ten krok powtarza się, aż wezwanie do __next__
rzutu a StopIteration
. Wyjątek jest połykany przez pętlę for i od tego momentu wykonywanie jest kontynuowane.
Wracając do generatorów: gdy wywołujesz __iter__
generator, po prostu sam się zwraca.
>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272
Oznacza to, że możesz oddzielić iterację od czegoś od tego, co chcesz z tym zrobić, i zmienić to zachowanie w połowie. Poniżej zauważ, jak ten sam generator jest używany w dwóch pętlach, a w drugim zaczyna działać od miejsca, w którym przerwał od pierwszego.
>>> generator = (x for x in 'more boring stuff')
>>> for letter in generator:
print(ord(letter))
if letter > 'p':
break
109
111
114
>>> for letter in generator:
print(letter)
e
b
o
r
i
n
g
s
t
u
f
f
Leniwa ocena
Jedną z wad generatorów w porównaniu z listami jest jedyna rzecz, do której można uzyskać dostęp w generatorze, to następna rzecz, która z niego wychodzi. Nie możesz cofnąć się i jak w przypadku poprzedniego wyniku lub przejść do następnego bez przechodzenia przez wyniki pośrednie. Zaletą tego jest to, że generator nie może zająć prawie żadnej pamięci w porównaniu do swojej równoważnej listy.
>>> import sys
>>> sys.getsizeof([x for x in range(10000)])
43816
>>> sys.getsizeof(range(10000000000))
24
>>> sys.getsizeof([x for x in range(10000000000)])
Traceback (most recent call last):
File "<pyshell#10>", line 1, in <module>
sys.getsizeof([x for x in range(10000000000)])
File "<pyshell#10>", line 1, in <listcomp>
sys.getsizeof([x for x in range(10000000000)])
MemoryError
Generatory mogą być również leniwie powiązane.
logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))
Pierwszy, drugi i trzeci wiersz po prostu definiują generator, ale nie wykonują żadnej prawdziwej pracy. Gdy wywoływany jest ostatni wiersz, suma prosi o kolumnę numeryczną o wartość, kolumna numeryczna potrzebuje wartości z ostatniej kolumny, ostatnia kolumna prosi o wartość z pliku dziennika, który następnie odczytuje wiersz z pliku. Stos ten rozwija się, dopóki suma nie otrzyma pierwszej liczby całkowitej. Następnie proces powtórzy się dla drugiej linii. W tym momencie suma ma dwie liczby całkowite i dodaje je do siebie. Zauważ, że trzeci wiersz nie został jeszcze odczytany z pliku. Sum następnie żąda wartości z kolumny liczbowej (całkowicie nieświadomy reszty łańcucha) i dodaje je, aż kolumna liczbowa się wyczerpie.
Naprawdę interesującą częścią jest to, że wiersze są czytane, konsumowane i odrzucane indywidualnie. W żadnym momencie cały plik w pamięci nie jest naraz. Co się stanie, jeśli ten plik dziennika to, powiedzmy, terabajt? Po prostu działa, ponieważ czyta tylko jedną linię na raz.
Wniosek
To nie jest pełny przegląd wszystkich zastosowań generatorów w Pythonie. W szczególności pominąłem nieskończone generatory, maszyny stanów, przekazując wartości z powrotem i ich związek z korupinami.
Uważam, że wystarczy wykazać, że możesz mieć generatory jako czysto zintegrowaną, przydatną funkcję językową.
yield
jest zasadniczo silnikiem stanu. Nie ma za każdym razem zwracać tego samego wyniku. Co to będzie zrobić z absolutną pewnością jest za każdym razem jest ona wywoływana powrócić następny element w przeliczalny. Wątki nie są wymagane; potrzebujesz zamknięcia (mniej więcej), aby utrzymać obecny stan.