Czy posiadanie funkcji języka generatora, takiego jak „fed”, to dobry pomysł?

9

PHP, C #, Python i prawdopodobnie kilka innych języków ma yieldsłowo kluczowe, które służy do tworzenia funkcji generatora.

W PHP: http://php.net/manual/en/language.generators.syntax.php

W języku Python: https://www.pythoncentral.io/python-generators-and-yield-keyword/

W języku C #: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/ke words / yield

Obawiam się, że jako funkcja / funkcja językowa yieldłamie niektóre konwencje. Jednym z nich jest „pewność”. Jest to metoda, która zwraca inny wynik za każdym razem, gdy ją wywołujesz. 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. Dlatego jeśli losowo wywołujesz funkcję generującą, nie znając jej poprzedniego stanu, nie możesz oczekiwać, że zwróci określony wynik.

Jak taka funkcja pasuje do paradygmatu językowego? Czy to faktycznie łamie jakieś konwencje? Czy warto mieć tę funkcję i korzystać z niej? (aby podać przykład tego, co dobre, a co złe, gotobyło kiedyś cechą wielu języków i nadal jest, ale jest uważane za szkodliwe i jako takie zostało wyeliminowane z niektórych języków, takich jak Java). Czy kompilatory / tłumacze języka programowania muszą zerwać z konwencjami, aby zaimplementować taką funkcję, na przykład, czy język musi implementować wielowątkowość, aby ta funkcja działała, czy może to być zrobione bez technologii wątkowania?

Dennis
źródło
4
yieldjest 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.
Robert Harvey,
1
Jeśli chodzi o jakość „pewności”, należy wziąć pod uwagę, że przy tej samej sekwencji wejściowej seria wywołań iteratora da dokładnie te same elementy w dokładnie tej samej kolejności.
Robert Harvey,
4
Nie jestem pewien, skąd pochodzi większość twoich pytań, ponieważ C ++ nie ma yield słowa kluczowego takiego jak Python. Ma metodę statyczną std::this_thread::yield(), ale to nie jest słowo kluczowe. Więc this_threadwstawiłby do niego prawie każde wywołanie, dzięki czemu byłoby dość oczywiste, że jest to funkcja biblioteczna tylko do generowania wątków, a nie funkcja językowa dotycząca generowania przepływu kontroli w ogóle.
Ixrec,
link zaktualizowany do C #, usunięto jeden dla C ++
Dennis

Odpowiedzi:

16

Najpierw zastrzeżenia - C # to język, który znam najlepiej, i chociaż jego język yieldwydaje się bardzo podobny do innych języków yield, mogą istnieć subtelne różnice, których nie znam.

Obawiam się, że jako funkcja / funkcja językowa, plon łamie niektóre konwencje. Jednym z nich jest „pewność”. Jest to metoda, która zwraca inny wynik za każdym razem, gdy ją wywołujesz.

Bzdury. Czy naprawdę oczekujesz Random.Nextlub Console.ReadLine zwracasz ten sam wynik za każdym razem, gdy do nich zadzwonisz? Co powiesz na połączenia Rest? Poświadczenie? Odebrać przedmiot z kolekcji? Istnieją różnego rodzaju (dobre, użyteczne) funkcje, które są nieczyste.

Jak taka funkcja pasuje do paradygmatu językowego? Czy to faktycznie łamie jakieś konwencje?

Tak, yieldgra bardzo źle try/catch/finallyi jest niedozwolony ( https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ dla więcej informacji).

Czy warto mieć tę funkcję i korzystać z niej?

Z pewnością warto mieć tę funkcję. Rzeczy takie jak LINQ w C # są naprawdę fajne - leniwe ocenianie kolekcji zapewnia dużą korzyść w zakresie wydajności i yieldpozwala na wykonanie tego rodzaju czynności w ułamku kodu z ułamkiem błędów, które zrobiłby iterator ręcznie walcowany.

To powiedziawszy, nie ma mnóstwo zastosowań yieldpoza przetwarzaniem kolekcji w stylu LINQ. Użyłem go do przetwarzania sprawdzania poprawności, generowania harmonogramu, randomizacji i kilku innych rzeczy, ale spodziewam się, że większość programistów nigdy go nie wykorzystała (lub niewłaściwie go wykorzystała).

Czy kompilatory / tłumacze języka programowania muszą zerwać z konwencjami, aby zaimplementować taką funkcję, na przykład, czy język musi implementować wielowątkowość, aby ta funkcja działała, czy może to być zrobione bez technologii wątkowania?

Nie dokładnie. Kompilator generuje iterator maszyny stanów, który śledzi, gdzie się zatrzymał, aby mógł się tam ponownie uruchomić przy następnym wywołaniu. Proces generowania kodu działa podobnie do stylu przekazywania kontynuacji, w którym kod po yieldjest wciągany do własnego bloku (a jeśli ma jakiś yields, inny podblok itd.). Jest to dobrze znane podejście stosowane częściej w programowaniu funkcjonalnym, a także pojawia się w kompilacji async / czekaj w języku C #.

Nie jest potrzebne wątkowanie, ale wymaga ono innego podejścia do generowania kodu w większości kompilatorów i ma pewien konflikt z innymi funkcjami językowymi.

Podsumowując, yieldjest to funkcja o stosunkowo niskim wpływie, która naprawdę pomaga przy określonym podziale problemów.

Telastyn
źródło
Nigdy nie używałem C # poważnie, ale to yieldsłowo kluczowe jest podobne do coroutines, tak, czy coś innego? Jeśli tak, chciałbym mieć jeden w C! Mogę wymyślić przynajmniej kilka porządnych części kodu, które byłyby o wiele łatwiejsze do napisania przy użyciu takiej funkcji językowej.
2
@DrunkCoder - podobny, ale z pewnymi ograniczeniami, jak rozumiem.
Telastyn,
1
Nie chciałbyś również, aby plony były niewłaściwie wykorzystywane. Im więcej funkcji ma dany język, tym bardziej prawdopodobne jest, że znajdziesz program źle napisany w tym języku. Nie jestem pewien, czy właściwym podejściem do pisania przystępnego języka jest rzucić na ciebie wszystko i zobaczyć, co się przyda.
Neil,
1
@DrunkCoder: jest to ograniczona wersja półkorup. W rzeczywistości kompilator traktuje go jako wzorzec składniowy, który zostaje rozszerzony na serię wywołań metod, klas i obiektów. (Zasadniczo kompilator generuje obiekt kontynuacji, który przechwytuje bieżący kontekst w polach.) Domyślną implementacją kolekcji jest półkorupina, ale przeciążając metody „magiczne” stosowane przez kompilator, można faktycznie dostosować zachowanie. Na przykład przed dodaniem async/ awaitdo języka ktoś go zaimplementował yield.
Jörg W Mittag,
1
@ Neil Zasadniczo możliwe jest niewłaściwe użycie praktycznie dowolnej funkcji języka programowania. Jeśli to, co mówisz, jest prawdą, wówczas znacznie trudniej byłoby źle zaprogramować za pomocą C niż Python lub C #, ale tak nie jest, ponieważ te języki mają wiele narzędzi, które chronią programistów przed wieloma błędami, które są bardzo łatwe zrobić z C. W rzeczywistości przyczyną złych programów są źli programiści - to dość problematyczny język.
Ben Cottrell,
12

Czy posiadanie funkcji języka generatora, takiego jak yielddobry 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ż yieldsł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 sumgeneratory 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ą.

Joel Harmon
źródło
6

Jeśli jesteś przyzwyczajony do klasycznych języków OOP, generatorów i yieldmoże wydawać się denerwujący, ponieważ stan zmienny jest przechwytywany na poziomie funkcji, a nie na poziomie obiektu.

Jednak kwestia „pewności” to czerwony śledź. Zwykle nazywa się to przezroczystością referencyjną i zasadniczo oznacza, że ​​funkcja zawsze zwraca ten sam wynik dla tych samych argumentów. Po osiągnięciu stanu zmiennego tracisz przejrzystość referencyjną. W OOP obiekty często mają stan zmienny, co oznacza, że ​​wynik wywołania metody nie zależy tylko od argumentów, ale także od stanu wewnętrznego obiektu.

Pytanie brzmi, gdzie uchwycić stan zmienny. W klasycznym OOP stan mutable istnieje na poziomie obiektu. Ale jeśli język obsługuje zamknięcia, możesz mieć stan zmienny na poziomie funkcji. Na przykład w JavaScript:

function getCounter() {
   var cnt = 1;
   return function(){ return cnt++; }
}
var counter = getCounter();
counter() --> 1
counter() --> 2

Krótko mówiąc, yieldjest naturalny w języku, który obsługuje zamykanie, ale byłby nie na miejscu w języku takim jak starsza wersja Java, gdzie stan zmienny istnieje tylko na poziomie obiektu.

JacquesB
źródło
Podejrzewam, że gdyby cechy językowe miały spektrum, wydajność byłaby tak daleka od funkcjonalności, jak mogłaby być. To niekoniecznie zła rzecz. OOP był kiedyś bardzo modny, a później programowanie funkcjonalne. Podejrzewam, że niebezpieczeństwo tego naprawdę sprowadza się do mieszania i dopasowywania funkcji, takich jak zysk, z funkcjonalnym projektem, który sprawia, że ​​Twój program zachowuje się w nieoczekiwany sposób.
Neil,
0

Moim zdaniem nie jest to dobra funkcja. Jest to zła cecha, przede wszystkim dlatego, że trzeba jej bardzo ostrożnie uczyć, a wszyscy źle ją uczą. Ludzie używają słowa „generator”, równoważąc funkcję generatora z obiektem generatora. Pytanie brzmi: kto lub co faktycznie wykonuje plony?

To nie jest tylko moja opinia. Nawet Guido w biuletynie PEP, w którym o tym rządzi, przyznaje, że funkcja generatora nie jest generatorem, ale „fabryką generatorów”.

To trochę ważne, nie sądzisz? Ale czytając 99% dokumentacji, można odnieść wrażenie, że funkcja generatora jest faktycznym generatorem i zwykle ignorują fakt, że potrzebujesz również obiektu generatora.

Guido rozważał zamianę „def” na „gen” dla tych funkcji i powiedział Nie. Ale twierdzę, że i tak by to nie wystarczyło. Naprawdę powinno to być:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
użytkownik320927
źródło