Avere una struttura linguistica del generatore come `yield` è una buona idea?


9

PHP, C #, Python e probabilmente alcune altre lingue hanno una yieldparola chiave che viene utilizzata per creare funzioni del generatore.

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

In Python: https://www.pythoncentral.io/python-generators-and-yield-keyword/

In C #: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield

Sono preoccupato che, come caratteristica / funzione del linguaggio, yieldrompa alcune convenzioni. Uno di questi è ciò a cui mi riferirei è "certezza". È un metodo che restituisce un risultato diverso ogni volta che lo chiami. Con una normale funzione non-generatore puoi chiamarlo e se gli viene dato lo stesso input, restituirà lo stesso output. Con rendimento, restituisce un output diverso, in base al suo stato interno. Pertanto, se si chiama in modo casuale la funzione di generazione, non conoscendo il suo stato precedente, non è possibile aspettarsi che restituisca un determinato risultato.

In che modo una funzione come questa si adatta al paradigma linguistico? In realtà infrange qualche convenzione? È una buona idea avere e utilizzare questa funzione? (per dare un esempio di ciò che è buono e ciò che è cattivo, gotouna volta era una caratteristica di molte lingue e lo è ancora, ma è considerato dannoso e come tale è stato sradicato da alcune lingue, come Java). I compilatori / interpreti del linguaggio di programmazione devono uscire dalle convenzioni per implementare tale funzione, ad esempio, un linguaggio deve implementare il multi-threading per far funzionare questa funzione o può essere fatto senza tecnologia di threading?


4
yieldè essenzialmente un motore statale. Non ha lo scopo di restituire lo stesso risultato ogni volta. Ciò che farà con assoluta certezza è restituire l'elemento successivo in un elenco numerabile ogni volta che viene invocato. I thread non sono richiesti; hai bisogno di una chiusura (più o meno), al fine di mantenere lo stato attuale.
Robert Harvey,

1
Per quanto riguarda la qualità della "certezza", considera che, data la stessa sequenza di input, una serie di chiamate all'iteratore produrrà esattamente gli stessi articoli esattamente nello stesso ordine.
Robert Harvey,

4
Non sono sicuro da dove provenga la maggior parte delle tue domande poiché C ++ non ha una yield parola chiave come Python. Ha un metodo statico std::this_thread::yield(), ma non è una parola chiave. Quindi this_threadanteponerebbe quasi ogni chiamata ad esso, rendendolo abbastanza ovvio che è una funzione di libreria solo per cedere thread, non una funzione di linguaggio per cedere flusso di controllo in generale.
Ixrec il

collegamento aggiornato a C #, rimosso uno per C ++
Dennis

Risposte:


16

Avvertenze innanzitutto: C # è la lingua che conosco meglio e, sebbene abbia una lingua yieldche sembra essere molto simile ad altre lingue yield, potrebbero esserci sottili differenze di cui non sono a conoscenza.

Sono preoccupato che come caratteristica / funzione del linguaggio, la resa rompe alcune convenzioni. Uno di questi è ciò a cui mi riferirei è "certezza". È un metodo che restituisce un risultato diverso ogni volta che lo chiami.

Sciocchezze. Ti aspetti davveroRandom.Next o Console.ReadLine di restituire lo stesso risultato ogni volta che li chiami? Che ne dici di chiamate Rest? Autenticazione? Ottieni un articolo da una collezione? Ci sono tutti i tipi di funzioni (buone, utili) che sono impure.

In che modo una funzione come questa si adatta al paradigma linguistico? In realtà infrange qualche convenzione?

Sì, yieldgioca davvero male try/catch/finally, ed è vietato ( https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ for Ulteriori informazioni).

È una buona idea avere e utilizzare questa funzione?

È sicuramente una buona idea avere questa funzione. Cose come LINQ di C # sono davvero piacevoli: valutare pigramente le raccolte offre un grande vantaggio in termini di prestazioni e yieldconsente di fare quel genere di cose in una frazione del codice con una frazione dei bug che un iteratore può eseguire manualmente.

Detto questo, non ci sono molti usi yieldal di fuori dell'elaborazione della raccolta in stile LINQ. L'ho usato per l'elaborazione di convalida, la generazione di programmi, la randomizzazione e alcune altre cose, ma mi aspetto che la maggior parte degli sviluppatori non l'abbia mai usato (o usato in modo improprio).

I compilatori / interpreti del linguaggio di programmazione devono uscire dalle convenzioni per implementare una tale funzione, ad esempio, un linguaggio deve implementare il multi-threading per far funzionare questa funzione o può essere fatto senza tecnologia di threading?

Non esattamente. Il compilatore genera un iteratore di macchina a stati che tiene traccia di dove si è fermato in modo che possa ricominciare da capo la prossima volta che viene chiamato. Il processo per la generazione del codice fa qualcosa di simile a Continuation Passing Style, in cui il codice dopo il yieldviene inserito nel proprio blocco (e se ha qualche yields, un altro sottoblocco e così via). Questo è un approccio ben noto usato più spesso nella programmazione funzionale e si presenta anche nella compilazione asincrona / wait di C #.

Non è necessario il threading, ma richiede un approccio diverso alla generazione del codice nella maggior parte dei compilatori e presenta alcuni conflitti con le funzionalità di altre lingue.

Tutto sommato, tuttavia, yieldè una funzione a impatto relativamente basso che aiuta davvero con un sottoinsieme specifico di problemi.


Non ho mai usato C # sul serio, ma questa yieldparola chiave è simile alle coroutine, sì o qualcosa di diverso? Se è così vorrei averne uno in C! Posso pensare ad almeno alcune decenti sezioni di codice che sarebbero state molto più facili da scrivere con una tale funzionalità linguistica.

2
@DrunkCoder - simile, ma con alcune limitazioni, a quanto ho capito.
Telastyn,

1
Inoltre, non vorrai vedere la resa abusata. Più funzionalità ha una lingua, più è probabile che troverai un programma scritto male in quella lingua. Non sono sicuro che l'approccio giusto alla scrittura di un linguaggio accessibile sia quello di lanciarti tutto e vedere cosa si attacca.
Neil,

1
@DrunkCoder: è una versione limitata di semi-coroutine. In realtà, viene considerato come un modello sintattico dal compilatore che viene espanso in una serie di chiamate a metodi, classi e oggetti. (Fondamentalmente, il compilatore genera un oggetto di continuazione che acquisisce il contesto corrente nei campi.) L' implementazione predefinita per le raccolte è un semi-coroutine, ma sovraccaricando i metodi "magici" utilizzati dal compilatore, è possibile personalizzare il comportamento. Ad esempio, prima async/ è awaitstato aggiunto alla lingua, qualcuno l'ha implementata usando yield.
Jörg W Mittag,

1
@Neil In genere è possibile utilizzare in modo improprio praticamente qualsiasi funzione del linguaggio di programmazione. Se ciò che dici fosse vero, sarebbe molto più difficile programmare male usando C di Python o C #, ma non è così poiché quei linguaggi hanno molti strumenti che proteggono i programmatori da molti degli errori che sono molto facili da fare con C. In realtà, la causa di cattivi programmi è cattivi programmatori - è un problema piuttosto linguistico.
Ben Cottrell,

12

Avere una struttura linguistica del generatore come yielduna buona idea?

Vorrei rispondere da una prospettiva Python con un enfatico sì, è un'ottima idea .

Inizierò affrontando prima alcune domande e ipotesi nella tua domanda, quindi dimostrerò la pervasività dei generatori e la loro irragionevolmente utile in Python in seguito.

Con una normale funzione non-generatore puoi chiamarlo e se gli viene dato lo stesso input, restituirà lo stesso output. Con rendimento, restituisce un output diverso, in base al suo stato interno.

Questo è falso I metodi sugli oggetti possono essere pensati come funzioni stesse, con il loro stato interno. In Python, poiché tutto è un oggetto, puoi effettivamente ottenere un metodo da un oggetto e passare quel metodo (che è legato all'oggetto da cui proviene, quindi ricorda il suo stato).

Altri esempi includono funzioni volutamente casuali e metodi di input come la rete, il file system e il terminale.

In che modo una funzione come questa si adatta al paradigma linguistico?

Se il paradigma del linguaggio supporta funzioni come le funzioni di prima classe e i generatori supportano altre funzionalità del linguaggio come il protocollo Iterable, allora si adattano perfettamente.

In realtà infrange qualche convenzione?

No. Dato che è integrato nella lingua, le convenzioni sono costruite attorno e includono (o richiedono!) L'uso di generatori.

I compilatori / interpreti del linguaggio di programmazione devono uscire dalle convenzioni per implementare tale funzione

Come con qualsiasi altra funzione, il compilatore deve semplicemente essere progettato per supportare la funzione. Nel caso di Python, le funzioni sono già oggetti con stato (come gli argomenti predefiniti e le annotazioni delle funzioni).

un linguaggio deve implementare il multi-threading per far funzionare questa funzione o può essere fatto senza tecnologia di threading?

Curiosità: l'implementazione predefinita di Python non supporta affatto il threading. È dotato di un Global Interpreter Lock (GIL), quindi nulla è effettivamente in esecuzione contemporaneamente a meno che non sia stato avviato un secondo processo per eseguire una diversa istanza di Python.


nota: gli esempi sono in Python 3

Oltre la resa

Mentre la yieldparola chiave può essere utilizzata in qualsiasi funzione per trasformarla in un generatore, non è l'unico modo per crearne una. Python presenta Generator Expressions, un modo potente per esprimere chiaramente un generatore in termini di un altro iterabile (inclusi altri generatori)

>>> 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

Come puoi vedere, non solo la sintassi è pulita e leggibile, ma le funzioni integrate come sumaccettano generatori.

Con

Dai un'occhiata alla proposta di potenziamento di Python per l' istruzione With . È molto diverso da quanto ci si potrebbe aspettare da un'istruzione With in altre lingue. Con un piccolo aiuto dalla libreria standard, i generatori di Python funzionano magnificamente come gestori di contesto per loro.

>>> 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

Naturalmente, stampare le cose è la cosa più noiosa che puoi fare qui, ma mostra risultati visibili. Le opzioni più interessanti includono l'autogestione delle risorse (apertura e chiusura di file / stream / connessioni di rete), blocco per concorrenza, wrapping o sostituzione temporanea di una funzione, decompressione e ricompressione dei dati. Se chiamare le funzioni è come iniettare codice nel tuo codice, allora con le istruzioni è come racchiudere parti del tuo codice in altri codici. Indipendentemente dal modo in cui lo usi, è un solido esempio di hook facile in una struttura linguistica. I generatori basati sul rendimento non sono l'unico modo per creare gestori di contesto, ma sono sicuramente convenienti.

Per e esaurimento parziale

Perché i loop in Python funzionano in modo interessante. Hanno il seguente formato:

for <name> in <iterable>:
    ...

Innanzitutto, l'espressione che ho chiamato <iterable>viene valutata per ottenere un oggetto iterabile. In secondo luogo, l'iterabile lo ha __iter__richiamato e l'iteratore risultante viene archiviato dietro le quinte. Successivamente, __next__viene chiamato sull'iteratore per ottenere un valore da associare al nome inserito <name>. Questo passaggio si ripete fino a quando la chiamata a __next__lancia a StopIteration. L'eccezione viene inghiottita dal ciclo for e l'esecuzione continua da lì.

Tornando ai generatori: quando si chiama __iter__un generatore, ritorna da solo.

>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272

Ciò significa che puoi separare l'iterazione su qualcosa dalla cosa che vuoi fare con essa e cambiare quel comportamento a metà strada. Di seguito, nota come lo stesso generatore viene utilizzato in due loop e nel secondo inizia l'esecuzione da dove si era interrotto dal primo.

>>> 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

Valutazione pigra

Uno dei lati negativi dei generatori rispetto alle liste è l'unica cosa a cui puoi accedere in un generatore è la prossima cosa che ne esce. Non è possibile tornare indietro e come per un risultato precedente o passare a un risultato successivo senza passare attraverso i risultati intermedi. Il lato positivo di questo è che un generatore può occupare quasi nessuna memoria rispetto al suo elenco equivalente.

>>> 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

I generatori possono anche essere concatenati pigramente.

logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))

La prima, la seconda e la terza riga definiscono ciascuna un generatore, ma non fanno alcun lavoro reale. Quando viene chiamata l'ultima riga, sum chiede a numiccolumn un valore, numiccolumn necessita di un valore da lastcolumn, lastcolumn richiede un valore dal file di log, che quindi legge effettivamente una riga dal file. Questo stack si svolge fino a quando la somma non ottiene il suo primo numero intero. Quindi, il processo si ripete per la seconda riga. A questo punto, la somma ha due numeri interi e li somma insieme. Si noti che la terza riga non è stata ancora letta dal file. La somma prosegue quindi richiedendo i valori da numiccolumn (totalmente ignaro del resto della catena) e aggiungendoli, fino a quando numiccolumn non si esaurisce.

La parte davvero interessante qui è che le righe vengono lette, consumate e scartate singolarmente. In nessun momento l'intero file è in memoria tutto in una volta. Cosa succede se questo file di registro è, diciamo, un terabyte? Funziona solo perché legge solo una riga alla volta.

Conclusione

Questa non è una recensione completa di tutti gli usi dei generatori in Python. In particolare, ho saltato infiniti generatori, macchine a stati, passando valori indietro e la loro relazione con le coroutine.

Credo che sia sufficiente dimostrare che si possono avere generatori come funzionalità di linguaggio utile e ben integrate.


6

Se sei abituato ai linguaggi OOP classici, i generatori e yieldpossono sembrare stonati perché lo stato mutabile viene acquisito a livello di funzione anziché a livello di oggetto.

La questione della "certezza" è però un'aringa rossa. Di solito si chiama trasparenza referenziale e sostanzialmente significa che la funzione restituisce sempre lo stesso risultato per gli stessi argomenti. Non appena si ha uno stato mutabile, si perde la trasparenza referenziale. In OOP, gli oggetti hanno spesso uno stato mutabile, il che significa che il risultato della chiamata al metodo non dipende solo dagli argomenti, ma anche dallo stato interno dell'oggetto.

La domanda è dove catturare lo stato mutabile. In un OOP classico, lo stato mutabile esiste a livello di oggetto. Ma se un supporto linguistico si chiude, è possibile che si verifichi uno stato modificabile a livello di funzione. Ad esempio in JavaScript:

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

In breve, yieldè naturale in un linguaggio che supporta le chiusure, ma sarebbe fuori posto in un linguaggio come la versione precedente di Java in cui lo stato mutabile esiste solo a livello di oggetto.


Suppongo che se le caratteristiche del linguaggio avessero uno spettro, il rendimento sarebbe il più lontano possibile dal punto di vista funzionale. Non è necessariamente una brutta cosa. OOP era una volta molto di moda, e di nuovo più tardi programmazione funzionale. Suppongo che il pericolo sia davvero quello di mescolare e abbinare caratteristiche come la resa con un design funzionale che fa sì che il tuo programma si comporti in modi inaspettati.
Neil,

0

Secondo me, non è una buona caratteristica. È una caratteristica negativa, soprattutto perché deve essere insegnata con molta attenzione e tutti la insegnano in modo sbagliato. Le persone usano la parola "generatore", equivocando tra la funzione generatore e l'oggetto generatore. La domanda è: solo chi o cosa sta facendo la resa effettiva?

Questa non è semplicemente la mia opinione. Perfino Guido, nel bollettino PEP in cui si pronuncia su questo, ammette che la funzione generatore non è un generatore ma una "fabbrica di generatori".

È un po 'importante, non credi? Ma leggendo il 99% della documentazione là fuori, avresti l'impressione che la funzione del generatore sia il vero generatore e tendono a ignorare il fatto che hai anche bisogno di un oggetto generatore.

Guido pensò di sostituire "def" con "gen" per queste funzioni e disse di no. Ma direi che non sarebbe bastato comunque. Dovrebbe davvero essere:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.