Alguém pode me dar um exemplo de por que existe a função "send" associada à função de gerador Python? Eu entendo perfeitamente a função de rendimento. No entanto, a função de envio é confusa para mim. A documentação deste método é complicada:
generator.send(value)
Retoma a execução e "envia" um valor para a função do gerador. O argumento do valor se torna o resultado da expressão de rendimento atual. O método send () retorna o próximo valor gerado pelo gerador ou gera StopIteration se o gerador sair sem gerar outro valor.
O que isso significa? Eu pensei que valor era a entrada para a função? A frase "O método send () retorna o próximo valor gerado pelo gerador" também parece ser o objetivo exato da função yield; yield retorna o próximo valor gerado pelo gerador ...
Alguém pode me dar um exemplo de gerador utilizando send que realiza algo que o rendimento não pode?
send()
é chamado para iniciar o gerador, ele deve ser chamado comNone
o argumento, porque não há expressão de rendimento que possa receber o valor.", Citado no documento oficial e para o qual a citação na pergunta é ausência de.Respostas:
É usado para enviar valores para um gerador que acabou de render. Aqui está um exemplo explicativo artificial (não útil):
Você não pode fazer isso apenas com
yield
.Quanto à utilidade, um dos melhores casos de uso que eu já vi é o Twisted
@defer.inlineCallbacks
. Essencialmente, ele permite que você escreva uma função como esta:O que acontece é que o
takesTwoSeconds()
retorno aDeferred
, que é um valor que promete um valor, será computado posteriormente. Twisted pode executar o cálculo em outro thread. Quando o cálculo é feito, ele é transferido para o adiado, e o valor é enviado de volta àdoStuff()
função. Assim, odoStuff()
pode acabar parecendo mais ou menos com uma função processual normal, exceto que pode estar fazendo todos os tipos de cálculos e retornos de chamada, etc. A alternativa antes dessa funcionalidade seria fazer algo como:É muito mais complicado e pesado.
fonte
Esta função é escrever corotinas
impressões
Veja como o controle está sendo passado para frente e para trás? Essas são corotinas. Eles podem ser usados para todos os tipos de coisas legais, como assíncrono IO e similares.
Pense assim, com um gerador e sem envio, é uma rua de mão única
Mas com o envio, torna-se uma via de mão dupla
O que abre a porta para o usuário personalizar o comportamento dos geradores em tempo real e o gerador responder ao usuário.
fonte
send()
o gerador ainda não alcançou a palavra-chaveyield
.Isso pode ajudar alguém. Aqui está um gerador que não é afetado pela função de envio. Ele recebe o parâmetro number na instanciação e não é afetado pelo envio:
Agora, aqui está como você faria o mesmo tipo de função usando send, para que em cada iteração você possa alterar o valor do número:
Aqui está o que parece, pois você pode ver que o envio de um novo valor para number altera o resultado:
Você também pode colocar isso em um loop for, como tal:
Para obter mais ajuda, consulte este ótimo tutorial .
fonte
send
? Um simpleslambda x: x * 2
faz a mesma coisa de uma maneira muito menos complicada.Alguns casos de uso para usar gerador e
send()
Geradores com
send()
permissão:Aqui estão alguns casos de uso:
Tentativa observada de seguir uma receita
Vamos ter uma receita, que espera um conjunto predefinido de entradas em alguma ordem.
Podemos:
watched_attempt
instância da receitacom cada verificação de entrada, se a entrada é a esperada (e falhará se não for)
Para usá-lo, primeiro crie a
watched_attempt
instância:A chamada para
.next()
é necessária para iniciar a execução do gerador.O valor retornado é exibido, nosso pote está vazio no momento.
Agora, faça poucas ações seguindo o que a receita espera:
Como vemos, o pote está finalmente vazio.
Caso não se seguisse a receita, ela falharia (o que poderia ser o resultado desejado da tentativa de cozinhar algo - apenas aprendendo que não prestamos atenção suficiente quando recebemos instruções.
Notar que:
Totais em execução
Podemos usar o gerador para acompanhar o total de valores enviados a ele.
Sempre que adicionamos um número, a contagem de entradas e a soma total são retornadas (válidas para o momento em que a entrada anterior foi enviada a ela).
A saída seria semelhante a:
fonte
O
send()
método controla qual será o valor à esquerda da expressão de rendimento.Para entender como o rendimento difere e qual o valor que ele possui, vamos primeiro atualizar rapidamente a ordem em que o código python é avaliado.
Seção 6.15 Ordem de avaliação
Portanto, uma expressão
a = b
do lado direito é avaliada primeiro.Como demonstrado a seguir,
a[p('left')] = p('right')
o lado direito é avaliado primeiro.O que yield produz ?, produz, suspende a execução da função e retorna ao chamador e retoma a execução no mesmo local em que parou antes da suspensão.
Onde exatamente a execução está suspensa? Você já deve ter adivinhado ... a execução está suspensa entre o lado direito e esquerdo da expressão de rendimento. Portanto,
new_val = yield old_val
a execução é interrompida no=
sinal e o valor à direita (que é antes da suspensão e também o valor retornado ao chamador) pode ser algo diferente do valor à esquerda (que é o valor que está sendo atribuído após a retomada) execução).yield
produz 2 valores, um para a direita e outro para a esquerda.Como você controla o valor no lado esquerdo da expressão de rendimento? através do
.send()
método6.2.9 Expressões de rendimento
fonte
O
send
método implementa corotinas .Se você não encontrou as Coroutines, elas são complicadas, porque mudam a maneira como o programa flui. Você pode ler um bom tutorial para mais detalhes.
fonte
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 dosend
função do gerador . Como interfaces singulares para suas respectivas extremidades do tubo,yield
esend
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, enquantosend
faz o oposto. Esse duplo dever é o cerne da confusão em torno da semântica de declarações comox = yield y
. Dividiryield
esend
dividir em duas etapas explícitas de push / pull tornará sua semântica muito mais clara:g
seja o gerador.g.send
empurra um valor para a esquerda pela extremidade direita do tubo.g
pausas, permitindo que o corpo da função do gerador funcione.g.send
é puxado para a esquerdayield
e recebido na extremidade esquerda do tubo. Inx = yield y
,x
é atribuído ao valor extraído.yield
seja alcançada.yield
empurra um valor para a direita pela extremidade esquerda do tubo, de volta parag.send
. Emx = yield y
,y
é empurrado para a direita através do tubo.g.send
retoma e puxa o valor e o retorna ao usuário.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 diferenteNone
da primeirasend
chamada). E pode ter um fim: quando não houver maisyield
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 dareturn
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 areturn
palavra - chave.) Quando umayield
instrução é encontrada, a função do gerador apenas faz uma pausa e, em seguida, retoma exatamente onde estava off ao receber outro valor. Esend
é 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
esend
são dois lados da mesmamoedapipe: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
evalue1 = g.send(value2)
cada um em duas instruções:left_end.push(y)
ex = left_end.pull()
; evalue1 = right_end.pull()
eright_end.push(value2)
. Existem dois casos especiais dayield
palavra-chave:x = yield
eyield y
. Estes são açúcar sintático, respectivamente, parax = 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 ag.send(None)
. Com isso em mente, podemos nos concentrar apenas em comosend
funciona e conversar apenas sobre o avanço do geradorsend
.Suponha que tenhamos
Agora, a definição de
f
desugars para a seguinte função comum (não geradora):O que aconteceu nesta transformação de
f
:left_end
acesso pela função aninhada e cujoright_end
retorno e acesso pelo escopo externo -right_end
é o que conhecemos como objeto gerador.left_end.pull()
éNone
, consumindo um valor empurrado no processo.x = yield y
foi substituída por duas linhas:left_end.push(y)
ex = left_end.pull()
.send
função pararight_end
, que é a contrapartida das duas linhas pelas quais substituímos ax = 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ídasright_end
e depoisimpl()
são chamadas. Portanto, em nosso exemplo acima, se seguíssemos a execução linha por linha, o que aconteceria é aproximadamente o seguinte: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:
Na maioria das vezes, eles desejam a mesma maneira que
f
, as únicas diferenças são como asyield
declarações são transformadas: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 momentopush
, então umUnboundLocalError
é aumentado.fonte
yield
?send
; leva uma chamada desend(None)
para mover o cursor para a primeirayield
instrução e somente então assend
chamadas subsequentes realmente enviam um valor "real" parayield
.f
seráyield
em algum momento e, portanto, aguarde até receber umasend
chamada do chamador? Com uma função normal, o intérprete começaria a executarf
imediatamente, certo? Afinal, não há compilação de AOT de qualquer tipo no Python. Tem certeza de que é o caso? (sem questionar o que você está dizendo, estou realmente intrigado com o que você escreveu aqui). Onde posso ler mais sobre como o Python sabe que precisa esperar antes de começar a executar o restante da função?send(None)
produzir o valor apropriado (por exemplo,1
) sem enviarNone
para o gerador sugere que a primeira chamada parasend
é um caso especial. É uma interface complicada para projetar; se você permitir que o primeirosend
envie um valor arbitrário, a ordem dos valores gerados e dos valores enviados será reduzida em um em comparação com o que é atualmente.Isso também me confundiu. Aqui está um exemplo que eu fiz ao tentar configurar um gerador que gera e aceita sinais em ordem alternada (rendimento, aceitação, produção, aceitação) ...
A saída é:
fonte