Avere una struttura linguistica del generatore come yield
una 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 yield
parola 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 sum
accettano 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.
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.