A palavra "rendimento" tem dois significados: produzir algo (por exemplo, produzir milho) e parar para deixar que outra pessoa continue (por exemplo, carros cedendo a pedestres). Ambas as definições se aplicam à yield
palavra-chave do Python ; o que torna as funções do gerador especiais é que, diferentemente das funções regulares, os valores podem ser "retornados" ao chamador enquanto apenas pausam, e não encerram, uma função do gerador.
É mais fácil imaginar um gerador como uma extremidade de um tubo bidirecional com uma extremidade "esquerda" e uma extremidade "direita"; esse tubo é o meio sobre o qual os valores são enviados entre o próprio gerador e o corpo da função do gerador. Cada extremidade do tubo possui duas operações push
:, que envia um valor e bloqueia até a outra extremidade do tubo extrair o valor e não retornar nada; epull
, que bloqueia até a outra extremidade do tubo empurra um valor e retorna o valor pressionado. No tempo de execução, a execução alterna entre os contextos de cada lado do canal - cada lado corre até enviar um valor para o outro lado; nesse ponto, ele pára, deixa o outro lado correr e aguarda um valor em return, altura em que o outro lado pára e continua. Em outras palavras, cada extremidade do canal passa do momento em que recebe um valor até o momento em que envia um valor.
O tubo é funcionalmente simétrico, mas - por convenção que estou definindo nesta resposta - a extremidade esquerda está disponível apenas dentro do corpo da função do gerador e é acessível através da yield
palavra - chave, enquanto a extremidade direita é o gerador e está acessível através do send
função do gerador . Como interfaces singulares para suas respectivas extremidades do tubo, yield
e send
cumprem um duplo dever: ambos empurram e puxam valores de / para suas extremidades do tubo, yield
empurrando para a direita e puxando para a esquerda, enquanto send
faz o oposto. Esse duplo dever é o cerne da confusão em torno da semântica de declarações como x = yield y
. Dividir yield
e send
dividir em duas etapas explícitas de push / pull tornará sua semântica muito mais clara:
- Suponha que
g
seja o gerador. g.send
empurra um valor para a esquerda pela extremidade direita do tubo.
- Execução no contexto de
g
pausas, permitindo que o corpo da função do gerador funcione.
- O valor pressionado
g.send
é puxado para a esquerda yield
e recebido na extremidade esquerda do tubo. In x = yield y
, x
é atribuído ao valor extraído.
- A execução continua dentro do corpo da função do gerador até que a próxima linha que contém
yield
seja alcançada.
yield
empurra um valor para a direita pela extremidade esquerda do tubo, de volta para g.send
. Em x = yield y
, y
é empurrado para a direita através do tubo.
- A execução dentro do corpo da função do gerador faz uma pausa, permitindo que o escopo externo continue de onde parou.
g.send
retoma e puxa o valor e o retorna ao usuário.
- Quando
g.send
é a próxima chamada, volte para a Etapa 1.
Embora cíclico, esse procedimento tem um começo: quando g.send(None)
- o que é next(g)
abreviado - é chamado pela primeira vez (é ilegal passar algo diferente None
da primeira send
chamada). E pode ter um fim: quando não houver mais yield
instruções a serem alcançadas no corpo da função do gerador.
Você vê o que torna a yield
declaração (ou mais precisamente, geradores) tão especial? Ao contrário da return
palavra-chave desprezível , yield
é capaz de passar valores para o chamador e receber valores do chamador sem interromper a função em que ele vive! (Obviamente, se você deseja encerrar uma função - ou um gerador - também é útil ter a return
palavra - chave.) Quando uma yield
instrução é encontrada, a função do gerador apenas faz uma pausa e, em seguida, retoma exatamente onde estava off ao receber outro valor. E send
é apenas a interface para se comunicar com a parte interna de uma função geradora de fora dela.
Se realmente queremos quebrar esse push / pull / analogia tubo para baixo, tanto quanto pudermos, vamos acabar com o seguinte pseudocódigo que realmente impulsiona casa que, além de etapas 1-5, yield
e send
são dois lados da mesma moeda pipe:
right_end.push(None) # the first half of g.send; sending None is what starts a generator
right_end.pause()
left_end.start()
initial_value = left_end.pull()
if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
left_end.do_stuff()
left_end.push(y) # the first half of yield
left_end.pause()
right_end.resume()
value1 = right_end.pull() # the second half of g.send
right_end.do_stuff()
right_end.push(value2) # the first half of g.send (again, but with a different value)
right_end.pause()
left_end.resume()
x = left_end.pull() # the second half of yield
goto 6
A transformação fundamental é que temos dividido x = yield y
e value1 = g.send(value2)
cada um em duas instruções: left_end.push(y)
e x = left_end.pull()
; e value1 = right_end.pull()
e right_end.push(value2)
. Existem dois casos especiais da yield
palavra-chave: x = yield
e yield y
. Estes são açúcar sintático, respectivamente, para x = yield None
e _ = yield y # discarding value
.
Para detalhes específicos sobre a ordem precisa em que os valores são enviados através do canal, veja abaixo.
O que se segue é um modelo concreto bastante longo do anterior. Primeiro, deve-se notar que, para qualquer gerador g
, next(g)
é exatamente equivalente a g.send(None)
. Com isso em mente, podemos nos concentrar apenas em como send
funciona e conversar apenas sobre o avanço do gerador send
.
Suponha que tenhamos
def f(y): # This is the "generator function" referenced above
while True:
x = yield y
y = x
g = f(1)
g.send(None) # yields 1
g.send(2) # yields 2
Agora, a definição de f
desugars para a seguinte função comum (não geradora):
def f(y):
bidirectional_pipe = BidirectionalPipe()
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
def impl():
initial_value = left_end.pull()
if initial_value is not None:
raise TypeError(
"can't send non-None value to a just-started generator"
)
while True:
left_end.push(y)
x = left_end.pull()
y = x
def send(value):
right_end.push(value)
return right_end.pull()
right_end.send = send
# This isn't real Python; normally, returning exits the function. But
# pretend that it's possible to return a value from a function and then
# continue execution -- this is exactly the problem that generators were
# designed to solve!
return right_end
impl()
O que aconteceu nesta transformação de f
:
- Movemos a implementação para uma função aninhada.
- Criamos um canal bidirecional cujo
left_end
acesso pela função aninhada e cujo right_end
retorno e acesso pelo escopo externo - right_end
é o que conhecemos como objeto gerador.
- Dentro da função aninhada, a primeira coisa que fazemos é verificar que
left_end.pull()
é None
, consumindo um valor empurrado no processo.
- Dentro da função aninhada, a instrução
x = yield y
foi substituída por duas linhas: left_end.push(y)
e x = left_end.pull()
.
- Definimos a
send
função para right_end
, que é a contrapartida das duas linhas pelas quais substituímos a x = yield y
instrução na etapa anterior.
Neste mundo de fantasia, onde as funções podem continuar após o retorno, g
são atribuídas right_end
e depois impl()
são chamadas. Portanto, em nosso exemplo acima, se seguíssemos a execução linha por linha, o que aconteceria é aproximadamente o seguinte:
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
y = 1 # from g = f(1)
# None pushed by first half of g.send(None)
right_end.push(None)
# The above push blocks, so the outer scope halts and lets `f` run until
# *it* blocks
# Receive the pushed value, None
initial_value = left_end.pull()
if initial_value is not None: # ok, `g` sent None
raise TypeError(
"can't send non-None value to a just-started generator"
)
left_end.push(y)
# The above line blocks, so `f` pauses and g.send picks up where it left off
# y, aka 1, is pulled by right_end and returned by `g.send(None)`
right_end.pull()
# Rinse and repeat
# 2 pushed by first half of g.send(2)
right_end.push(2)
# Once again the above blocks, so g.send (the outer scope) halts and `f` resumes
# Receive the pushed value, 2
x = left_end.pull()
y = x # y == x == 2
left_end.push(y)
# The above line blocks, so `f` pauses and g.send(2) picks up where it left off
# y, aka 2, is pulled by right_end and returned to the outer scope
right_end.pull()
x = left_end.pull()
# blocks until the next call to g.send
Isso é mapeado exatamente para o pseudocódigo de 16 etapas acima.
Existem outros detalhes, como a propagação de erros e o que acontece quando você chega ao final do gerador (o tubo está fechado), mas isso deve deixar claro como o fluxo de controle básico funciona quando send
é usado.
Usando essas mesmas regras de remoção de açúcar, vejamos dois casos especiais:
def f1(x):
while True:
x = yield x
def f2(): # No parameter
while True:
x = yield x
Na maioria das vezes, eles desejam a mesma maneira que f
, as únicas diferenças são como as yield
declarações são transformadas:
def f1(x):
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
def f2():
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
No primeiro, o valor passado para f1
é empurrado (gerado) inicialmente e, em seguida, todos os valores puxados (enviados) são empurrados (retornados) de volta. No segundo, x
ainda não tem valor quando chega o momento push
, então um UnboundLocalError
é aumentado.