Ter uma facilidade de linguagem geradora como yield
uma 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 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)
>>> 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 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.
>>> 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.
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.