Ter uma facilidade de linguagem geradora como o `yield` é uma boa idéia?

9

PHP, C #, Python e provavelmente algumas outras linguagens possuem uma yieldpalavra - 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, yieldquebre 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, gotojá 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?

Dennis
fonte
4
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.
Robert Harvey
11
Quanto à qualidade da "certeza", considere que, dada a mesma sequência de entrada, uma série de chamadas para o iterador produzirá exatamente os mesmos itens, exatamente na mesma ordem.
Robert Harvey
4
Não sei de onde vem a maioria das suas perguntas, pois o C ++ não possui uma yield palavra - chave como o Python. Ele tem um método estático std::this_thread::yield(), mas isso não é uma palavra-chave. Portanto, ele this_threadincluiria 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.
Ixrec
link atualizado para C #, um para C ++ retirados
Dennis

Respostas:

16

Advertências primeiro - o C # é o idioma que eu conheço melhor e, embora tenha um yieldque pareça muito semelhante ao de outros idiomas yield, pode haver diferenças sutis que eu desconheço.

Estou preocupado que, como recurso / recurso de idioma, o rendimento 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.

Tolices. Você realmente espera Random.Nextou Console.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.

Como uma função como essa se encaixa no paradigma da linguagem? Ele realmente quebra algumas convenções?

Sim, yieldjoga muito mal try/catch/finallye 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).

É uma boa ideia ter e usar esse recurso?

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 yieldpermite 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 yieldfora 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).

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?

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 algum yields, 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.

Telastyn
fonte
Eu nunca usei o C # a sério, mas essa yieldpalavra-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.
2
@DrunkCoder - semelhante, mas com algumas limitações, pelo que entendi.
Telastyn
11
Você também não gostaria que o rendimento fosse mal utilizado. Quanto mais recursos um idioma tiver, maior será a probabilidade de você encontrar um programa mal escrito nesse idioma. Não tenho certeza se a abordagem correta para escrever uma linguagem acessível é jogar tudo em você e ver o que fica.
21717 Neil
11
@DrunkCoder: é uma versão limitada de semi-corotinas. Na verdade, ele é tratado como um padrão sintático pelo compilador que é expandido em uma série de chamadas, classes e objetos de método. (Basicamente, o compilador gera um objeto de continuação que captura o contexto atual nos campos.) A implementação padrão para coleções é uma semicorotina, mas sobrecarregando os métodos "mágicos" que o compilador usa, você pode realmente personalizar o comportamento. Por exemplo, antes async/ awaitfoi adicionado ao idioma, alguém o implementou usando yield.
Jörg W Mittag
11
@ Neil Geralmente, é possível usar praticamente qualquer recurso da linguagem de programação. Se o que você diz é verdade, seria muito mais difícil programar mal usando C do que Python ou C #, mas esse não é o caso, pois essas linguagens têm muitas ferramentas que protegem os programadores de muitos dos erros que são muito fáceis. fazer com C. Na realidade, a causa de programas ruins são programadores ruins - é um problema independente de idioma.
precisa
12

Ter uma facilidade de linguagem geradora como yielduma boa ideia?

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.

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.

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.

Como uma função como essa se encaixa no paradigma da linguagem?

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.

Ele realmente quebra algumas convenções?

Não. Como está embutida no idioma, as convenções são construídas e incluem (ou exigem!) O uso de geradores.

Os compiladores / intérpretes da linguagem de programação precisam interromper quaisquer convenções para implementar esse recurso

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

um idioma precisa implementar a multiencadeamento para que esse recurso funcione ou pode ser feito sem a tecnologia de encadeamento?

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

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

Como você pode ver, não apenas a sintaxe é limpa e legível, mas também as funções suminternas, 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.

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

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:

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

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 a StopIteration. 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.

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

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.

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

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.

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

Os geradores também podem ser acorrentados preguiçosamente.

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

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.

Joel Harmon
fonte
6

Se você está acostumado a linguagens OOP clássicas, geradores e yieldpode 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:

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

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.

JacquesB
fonte
Suponho que se os recursos da linguagem tivessem um espectro, o rendimento ficaria o mais longe possível do funcional. Isso não é necessariamente uma coisa ruim. OOP era uma vez muito na moda, e novamente mais tarde programação funcional. Suponho que o perigo disso realmente seja misturar e combinar recursos como rendimento com um design funcional que faz com que seu programa se comporte de maneiras inesperadas.
Neil
0

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:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
user320927
fonte