PHP, C #, Python e provavelmente algumas outras linguagens possuem uma yield
palavra - chave usada para criar funções geradoras.
No PHP: http://php.net/manual/en/language.generators.syntax.php
Em Python: https://www.pythoncentral.io/python-generators-and-yield-keyword/
Em C #: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield
Estou preocupado que, como um recurso / recurso de idioma, yield
quebre algumas convenções. Um deles é o que eu me referiria é "certeza". É um método que retorna um resultado diferente toda vez que você o chama. Com uma função regular não geradora, você pode chamá-lo e, se receber a mesma entrada, retornará a mesma saída. Com rendimento, ele retorna uma saída diferente, com base em seu estado interno. Portanto, se você chamar aleatoriamente a função geradora, sem conhecer seu estado anterior, não poderá esperar que ela retorne um determinado resultado.
Como uma função como essa se encaixa no paradigma da linguagem? Ele realmente quebra algumas convenções? É uma boa ideia ter e usar esse recurso? (para dar um exemplo do que é bom e do que é ruim, goto
já foi um recurso de muitas linguagens e ainda é, mas é considerado prejudicial e, como tal, foi erradicado de algumas linguagens, como Java). Os compiladores / intérpretes da linguagem de programação precisam interromper algumas convenções para implementar esse recurso, por exemplo, um idioma precisa implementar o multiencadeamento para que esse recurso funcione, ou pode ser feito sem a tecnologia de encadeamento?
yield
é essencialmente um mecanismo de estado. Não se destina a retornar o mesmo resultado todas as vezes. O que ele fará com certeza absoluta é retornar o próximo item em um enumerável cada vez que for chamado. Threads não são necessários; você precisa de um fechamento (mais ou menos) para manter o estado atual.yield
palavra - chave como o Python. Ele tem um método estáticostd::this_thread::yield()
, mas isso não é uma palavra-chave. Portanto, elethis_thread
incluiria quase todas as chamadas, tornando bastante óbvio que é um recurso de biblioteca apenas para gerar threads, não um recurso de linguagem sobre como gerar fluxo de controle em geral.Respostas:
Advertências primeiro - o C # é o idioma que eu conheço melhor e, embora tenha um
yield
que pareça muito semelhante ao de outros idiomasyield
, pode haver diferenças sutis que eu desconheço.Tolices. Você realmente espera
Random.Next
ouConsole.ReadLine
devolve o mesmo resultado toda vez que liga para eles? E as chamadas Rest? Autenticação? Obter item de uma coleção? Existem todos os tipos de funções (boas, úteis) que são impuras.Sim,
yield
joga muito maltry/catch/finally
e não é permitido ( https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ para mais informações).Certamente é uma boa ideia ter esse recurso. Coisas como o LINQ do C # são realmente legais - avaliar preguiçosamente as coleções fornece um grande benefício de desempenho e
yield
permite que esse tipo de coisa seja feito em uma fração do código com uma fração dos bugs que um iterador rolado manualmente faria.Dito isto, não há muitos usos para
yield
fora do processamento da coleção de estilos LINQ. Usei-o para processamento de validação, geração de agendamento, randomização e algumas outras coisas, mas espero que a maioria dos desenvolvedores nunca o tenha usado (ou usado mal).Não exatamente. O compilador gera um iterador de máquina de estado que monitora onde parou, para que possa iniciar lá novamente na próxima vez que for chamado. O processo para geração de código faz algo semelhante ao Estilo de passagem de continuação, em que o código após o
yield
é puxado para seu próprio bloco (e se tiver algumyield
s, outro sub-bloco e assim por diante). Essa é uma abordagem bem conhecida usada com mais frequência na Programação Funcional e também aparece na compilação assíncrona / aguardada do C #.Nenhum encadeamento é necessário, mas requer uma abordagem diferente para a geração de código na maioria dos compiladores e tem algum conflito com outros recursos de idioma.
Em suma,
yield
é um recurso de impacto relativamente baixo que realmente ajuda com um subconjunto específico de problemas.fonte
yield
palavra-chave é semelhante às corotinas, sim, ou algo diferente? Se sim, eu gostaria de ter um em C! Posso pensar em pelo menos algumas seções decentes de código que teriam sido muito mais fáceis de escrever com esse recurso de linguagem.async
/await
foi adicionado ao idioma, alguém o implementou usandoyield
.Eu gostaria de responder isso de uma perspectiva Python com um enfático sim, é uma ótima idéia .
Começarei abordando algumas perguntas e suposições em sua pergunta primeiro e depois demonstrarei a difusão de geradores e sua utilidade irracional em Python posteriormente.
Isto é falso. Métodos em objetos podem ser pensados como funções em si, com seu próprio estado interno. No Python, como tudo é um objeto, é possível obter um método de um objeto e transmiti-lo (que está vinculado ao objeto de origem, para que ele se lembre de seu estado).
Outros exemplos incluem funções aleatoriamente deliberadas, bem como métodos de entrada como a rede, sistema de arquivos e terminal.
Se o paradigma da linguagem suportar coisas como funções de primeira classe e os geradores suportarem outros recursos da linguagem, como o protocolo Iterable, eles se encaixam perfeitamente.
Não. Como está embutida no idioma, as convenções são construídas e incluem (ou exigem!) O uso de geradores.
Como em qualquer outro recurso, o compilador simplesmente precisa ser projetado para suportar o recurso. No caso do Python, funções já são objetos com estado (como argumentos padrão e anotações de função).
Curiosidade: a implementação padrão do Python não oferece suporte a threading. Ele possui um Global Interpreter Lock (GIL), então nada está sendo executado simultaneamente, a menos que você tenha acionado um segundo processo para executar uma instância diferente do Python.
nota: exemplos estão em Python 3
Além do rendimento
Embora a
yield
palavra - chave possa ser usada em qualquer função para transformá-la em um gerador, não é a única maneira de criar uma. O Python possui Expressões de Gerador, uma maneira poderosa de expressar claramente um gerador em termos de outro iterável (incluindo outros geradores)Como você pode ver, não apenas a sintaxe é limpa e legível, mas também as funções
sum
internas, como aceitar geradores.Com
Confira a Proposta de aprimoramento do Python para a declaração With . É muito diferente do que você poderia esperar de uma declaração With em outros idiomas. Com uma pequena ajuda da biblioteca padrão, os geradores do Python funcionam lindamente como gerenciadores de contexto para eles.
Obviamente, imprimir coisas é a coisa mais chata que você pode fazer aqui, mas mostra resultados visíveis. As opções mais interessantes incluem gerenciamento automático de recursos (abertura e fechamento de arquivos / fluxos / conexões de rede), bloqueio por simultaneidade, quebra ou substituição temporária de uma função e descompactação e recompactação de dados. Se chamar funções é como injetar código no seu código, então com instruções é como agrupar partes do seu código em outro código. Seja como for, é um exemplo sólido de um gancho fácil em uma estrutura de linguagem. Geradores baseados em rendimento não são a única maneira de criar gerenciadores de contexto, mas certamente são convenientes.
Esgotamento parcial e parcial
Os loops no Python funcionam de uma maneira interessante. Eles têm o seguinte formato:
Primeiro, a expressão que chamei
<iterable>
é avaliada para obter um objeto iterável. Segundo, o iterável o__iter__
chamou e o iterador resultante é armazenado nos bastidores. Posteriormente,__next__
é chamado no iterador para obter um valor para vincular ao nome que você colocou<name>
. Esta etapa se repete até que a chamada__next__
jogue aStopIteration
. A exceção é engolida pelo loop for e a execução continua a partir daí.Voltando aos geradores: quando você liga
__iter__
para um gerador, ele simplesmente retorna.O que isso significa é que você pode separar a iteração sobre algo da coisa que deseja fazer com ele e mudar esse comportamento no meio do caminho. Abaixo, observe como o mesmo gerador é usado em dois loops e, no segundo, começa a executar de onde parou do primeiro.
Avaliação preguiçosa
Uma das desvantagens dos geradores, em comparação com as listas, é a única coisa que você pode acessar em um gerador e a próxima coisa que sai dele. Você não pode voltar atrás e obter um resultado anterior, ou pular para um resultado posterior sem passar pelos resultados intermediários. O lado positivo disso é que um gerador pode ocupar quase nenhuma memória em comparação com sua lista equivalente.
Os geradores também podem ser acorrentados preguiçosamente.
A primeira, segunda e terceira linhas definem apenas um gerador cada, mas não realizam nenhum trabalho real. Quando a última linha é chamada, sum pede a numericcolumn por um valor, numiccolumn precisa de um valor de lastcolumn, lastcolumn solicita um valor de logfile, que na verdade lê uma linha do arquivo. Essa pilha se desdobra até que soma receba seu primeiro número inteiro. Então, o processo acontece novamente para a segunda linha. Nesse ponto, soma possui dois números inteiros e os soma. Observe que a terceira linha ainda não foi lida no arquivo. Sum então solicita valores da coluna numérica (totalmente alheio ao restante da cadeia) e os adiciona até que a coluna numérica se esgote.
A parte realmente interessante aqui é que as linhas são lidas, consumidas e descartadas individualmente. Em nenhum momento o arquivo inteiro está na memória de uma só vez. O que acontece se esse arquivo de log for, digamos, um terabyte? Apenas funciona, porque lê apenas uma linha de cada vez.
Conclusão
Esta não é uma revisão completa de todos os usos de geradores em Python. Notavelmente, pulei infinitos geradores, máquinas de estado, passando valores de volta e seu relacionamento com corotinas.
Acredito que seja suficiente demonstrar que você pode ter geradores como um recurso de linguagem útil e bem integrado.
fonte
Se você está acostumado a linguagens OOP clássicas, geradores e
yield
pode parecer dissonante porque o estado mutável é capturado no nível da função e não no nível do objeto.A questão da "certeza" é um arenque vermelho. Geralmente é chamado de transparência referencial e basicamente significa que a função sempre retorna o mesmo resultado para os mesmos argumentos. Assim que você tiver um estado mutável, você perde a transparência referencial. No POO, os objetos geralmente têm um estado mutável, o que significa que o resultado da chamada do método não depende apenas dos argumentos, mas também do estado interno do objeto.
A questão é onde capturar o estado mutável. Em um POO clássico, o estado mutável existe no nível do objeto. Mas se um suporte de idioma for fechado, você poderá ter um estado mutável no nível da função. Por exemplo em JavaScript:
Em resumo,
yield
é natural em uma linguagem que suporta fechamentos, mas estaria deslocada em uma linguagem como a versão mais antiga do Java, onde o estado mutável existe apenas no nível do objeto.fonte
Na minha opinião, não é uma boa característica. É uma característica ruim, principalmente porque precisa ser ensinada com muito cuidado e todos ensinam errado. As pessoas usam a palavra "gerador", equivocando entre a função do gerador e o objeto gerador. A questão é: quem ou o que está realmente produzindo?
Esta não é apenas minha opinião. Até Guido, no boletim do PEP em que ele decide isso, admite que a função de gerador não é um gerador, mas uma "fábrica de geradores".
Isso é meio importante, você não acha? Mas lendo 99% da documentação existente, você terá a impressão de que a função do gerador é o gerador real e eles tendem a ignorar o fato de que você também precisa de um objeto gerador.
Guido pensou em substituir "def" por "gen" para essas funções e disse não. Mas eu argumentaria que isso não seria suficiente. Deveria ser realmente:
fonte