Expressões do gerador versus compreensão da lista

412

Quando você deve usar expressões geradoras e quando deve usar a compreensão de lista no Python?

# Generator expression
(x*2 for x in range(256))

# List comprehension
[x*2 for x in range(256)]
Somente leitura
fonte
28
poderia [exp for x in iter]ser apenas açúcar list((exp for x in iter))? ou existe uma diferença de execução?
b0fh
1
acha que eu tinha uma pergunta relevante; portanto, ao usar yield, podemos usar apenas a expressão geradora de uma função ou precisamos usar yield para que uma função retorne o objeto gerador?
28
@ b0fh Resposta tardia ao seu comentário: no Python2 há uma pequena diferença, a variável loop vazará da compreensão da lista, enquanto a expressão do gerador não vazará. Compare X = [x**2 for x in range(5)]; print xcom Y = list(y**2 for y in range(5)); print y, o segundo dará um erro. No Python3, uma compreensão de lista é realmente o açúcar sintático para uma expressão de gerador alimentada list()conforme o esperado, portanto a variável loop não vazará mais .
Bas Swinckels
13
Eu sugiro ler PEP 0289 . Resumido por "Este PEP apresenta expressões de gerador como uma generalização eficiente de memória e alto desempenho de compreensões e geradores de lista" . Também possui exemplos úteis de quando usá-los.
icc97
5
@ icc97 Também estou oito anos atrasado para a festa, e o link do PEP foi perfeito. Obrigado por tornar isso fácil de encontrar!
eenblam

Respostas:

283

A resposta de John é boa (essa compreensão da lista é melhor quando você deseja repetir várias vezes). No entanto, também vale a pena notar que você deve usar uma lista se quiser usar qualquer um dos métodos da lista. Por exemplo, o seguinte código não funcionará:

def gen():
    return (something for something in get_some_stuff())

print gen()[:2]     # generators don't support indexing or slicing
print [5,6] + gen() # generators can't be added to lists

Basicamente, use uma expressão geradora se tudo o que você estiver fazendo for iterando uma vez. Se você deseja armazenar e usar os resultados gerados, provavelmente está melhor com uma compreensão da lista.

Como o desempenho é o motivo mais comum para escolher um sobre o outro, meu conselho é não se preocupar com isso e apenas escolher um; se você achar que seu programa está sendo executado muito devagar, só então você deve voltar e se preocupar em ajustar seu código.

Eli Courtwright
fonte
70
Às vezes, você precisa usar geradores - por exemplo, se estiver escrevendo corotinas com agendamento cooperativo usando rendimento. Mas se você está fazendo isso, você provavelmente não está fazendo esta pergunta;)
ephemient
12
Eu sei que isso é antigo, mas acho que vale a pena notar que geradores (e qualquer iterável) podem ser adicionados a listas com extensão: a = [1, 2, 3] b = [4, 5, 6] a.extend(b)- a agora será [1, 2, 3, 4, 5, 6]. (Você pode adicionar novas linhas em comentários ??)
jarvisteve
12
@jarvisteve seu exemplo esconde as palavras que você está dizendo. Também há um bom argumento aqui. As listas podem ser estendidas com geradores, mas não havia sentido em torná-lo um gerador. Os geradores não podem ser estendidos com listas e os geradores não são completamente iteráveis. a = (x for x in range(0,10)), b = [1,2,3]por exemplo. a.extend(b)lança uma exceção. b.extend(a)avaliará tudo, nesse caso, não há sentido em torná-lo um gerador em primeiro lugar.
Slater Victoroff
4
@SlaterTyranus, você está 100% correto, e eu votei para você por precisão. no entanto, acho que o comentário dele é uma não resposta útil à pergunta do OP, porque ajudará aqueles que se encontram aqui porque digitaram algo como 'combinar gerador com compreensão de lista' em um mecanismo de pesquisa.
Rbp
1
O motivo do uso de um gerador para iterar uma vez (por exemplo, minha preocupação com a falta de memória substitui minha preocupação em "buscar" valores um de cada vez ) provavelmente ainda se aplica ao iterar várias vezes? Eu diria que isso pode tornar uma lista mais útil, mas se isso é suficiente para superar as preocupações de memória é outra coisa.
Rob Grant
181

A iteração sobre a expressão do gerador ou a compreensão da lista fará a mesma coisa. No entanto, a compreensão da lista criará a lista inteira na memória primeiro, enquanto a expressão do gerador criará os itens rapidamente, para que você possa usá-lo para sequências muito grandes (e também infinitas!).

dF.
fonte
39
+1 para infinito. Você não pode fazer isso com uma lista, independentemente de quão pouco se importe com o desempenho.
Paul Draper
Você pode criar geradores infinitos usando o método de compreensão?
AnnanFay
5
@ Annan Apenas se você já tiver acesso a outro gerador infinito. Por exemplo, itertools.count(n)é uma sequência infinita de números inteiros, iniciando em n, portanto (2 ** item for item in itertools.count(n))seria uma sequência infinita dos poderes de 2começar em 2 ** n.
Kevin
2
Um gerador exclui itens da memória após a iteração. Portanto, é rápido se você tiver grandes dados, apenas deseja exibi-los, por exemplo. Não é um porco da memória. com geradores, os itens são processados ​​'conforme necessário'. se você deseja manter a lista ou iterá-la novamente (para armazenar os itens), use a compreensão da lista.
J2emanue
102

Use a compreensão da lista quando o resultado precisar ser repetido várias vezes ou quando a velocidade for primordial. Use expressões geradoras onde o intervalo é grande ou infinito.

Consulte Expressões do gerador e compreensões da lista para obter mais informações.

John Millikin
fonte
2
Isso provavelmente será um pouco fora de tópico, mas, infelizmente, "incontrolável" ... O que "primordial" significaria nesse contexto? Eu não sou um falante nativo de inglês ... :)
Guillermo Ares
6
@GuillermoAres, este é o resultado direto de "pesquisa" no significado de supremacia: mais importante do que qualquer outra coisa; supremo.
31716
1
Então, listssão mais rápidos que generatorexpressões? Ao ler a resposta da dF, descobriu-se que era o contrário.
Hassan Baig
1
Provavelmente é melhor dizer que a compreensão da lista é mais rápida quando o intervalo é pequeno, mas à medida que a escala aumenta, torna-se mais valioso calcular os valores em tempo real - bem a tempo de serem usados. É isso que uma expressão geradora faz.
Kyle
59

O ponto importante é que a compreensão da lista cria uma nova lista. O gerador cria um objeto iterável que "filtra" o material de origem rapidamente enquanto você consome os bits.

Imagine que você tem um arquivo de log de 2 TB chamado "hugefile.txt" e deseja o conteúdo e o comprimento de todas as linhas que começam com a palavra "ENTRY".

Então, tente começar escrevendo uma lista de compreensão:

logfile = open("hugefile.txt","r")
entry_lines = [(line,len(line)) for line in logfile if line.startswith("ENTRY")]

Isso reduz o arquivo inteiro, processa cada linha e armazena as linhas correspondentes em sua matriz. Essa matriz pode, portanto, conter até 2 TB de conteúdo. É muita RAM e provavelmente não é prático para seus propósitos.

Então, em vez disso, podemos usar um gerador para aplicar um "filtro" ao nosso conteúdo. Nenhum dado é realmente lido até começarmos a iterar sobre o resultado.

logfile = open("hugefile.txt","r")
entry_lines = ((line,len(line)) for line in logfile if line.startswith("ENTRY"))

Ainda nem uma única linha foi lida em nosso arquivo. De fato, digamos que queremos filtrar ainda mais nosso resultado:

long_entries = ((line,length) for (line,length) in entry_lines if length > 80)

Ainda nada foi lido, mas especificamos agora dois geradores que atuarão em nossos dados conforme desejamos.

Vamos escrever nossas linhas filtradas para outro arquivo:

outfile = open("filtered.txt","a")
for entry,length in long_entries:
    outfile.write(entry)

Agora lemos o arquivo de entrada. Como nosso forloop continua solicitando linhas adicionais, o long_entriesgerador exige linhas do entry_linesgerador, retornando apenas aquelas cujo comprimento é superior a 80 caracteres. Por sua vez, o entry_linesgerador solicita linhas (filtradas conforme indicado) ao logfileiterador, que por sua vez lê o arquivo.

Portanto, em vez de "enviar" dados para sua função de saída na forma de uma lista totalmente preenchida, você está dando à função de saída uma maneira de "extrair" dados apenas quando necessário. No nosso caso, isso é muito mais eficiente, mas não tão flexível. Geradores são uma maneira, uma passagem; os dados do arquivo de log que lemos são imediatamente descartados; portanto, não podemos voltar à linha anterior. Por outro lado, não precisamos nos preocupar em manter os dados por perto quando terminarmos.

tylerl
fonte
46

O benefício de uma expressão geradora é que ela usa menos memória, pois não cria a lista inteira de uma só vez. As expressões de gerador são mais usadas quando a lista é um intermediário, como a soma dos resultados ou a criação de um ditado a partir dos resultados.

Por exemplo:

sum(x*2 for x in xrange(256))

dict( (k, some_func(k)) for k in some_list_of_keys )

A vantagem é que a lista não é completamente gerada e, portanto, pouca memória é usada (e também deve ser mais rápida)

No entanto, você deve usar a compreensão da lista quando o produto final desejado for uma lista. Você não vai salvar nenhuma memória usando expressões geradoras, pois deseja a lista gerada. Você também tem o benefício de poder usar qualquer uma das funções da lista, como classificada ou revertida.

Por exemplo:

reversed( [x*2 for x in xrange(256)] )
Mandril
fonte
9
Existe uma dica para você na linguagem de que expressões geradoras devem ser usadas dessa maneira. Perca os suportes! sum(x*2 for x in xrange(256))
u0b34a0f6ae 29/09/09
8
sortede reversedfuncione bem em quaisquer expressões geradoras iteráveis ​​incluídas.
27513 marrola
1
Se você pode usar 2.7 e acima, que dict () exemplo seria parece melhor como uma compreensão dict (o PEP para que é mais velho do que então o PEP gerador de expressões, mas levou mais tempo para terra)
Jürgen A. Erhard
14

Ao criar um gerador a partir de um objeto mutável (como uma lista), lembre-se de que o gerador será avaliado no estado da lista no momento da utilização do gerador, não no momento da criação do gerador:

>>> mylist = ["a", "b", "c"]
>>> gen = (elem + "1" for elem in mylist)
>>> mylist.clear()
>>> for x in gen: print (x)
# nothing

Se houver alguma chance de sua lista ser modificada (ou um objeto mutável dentro dessa lista), mas você precisar do estado na criação do gerador, precisará usar uma compreensão da lista.

freaker
fonte
1
E esta deve ser a resposta aceita. Se seus dados forem maiores que a memória disponível, você sempre deve usar geradores, embora a lista de repetições na memória possa ser mais rápida (mas você não tem memória suficiente para isso).
Marek Marczak 28/08/19
4

Às vezes, você pode se livrar da função tee dos itertools , que retorna vários iteradores para o mesmo gerador que pode ser usado independentemente.

Jacob Rigby
fonte
4

Estou usando o módulo Hadoop Mincemeat . Eu acho que este é um ótimo exemplo para anotar:

import mincemeat

def mapfn(k,v):
    for w in v:
        yield 'sum',w
        #yield 'count',1


def reducefn(k,v): 
    r1=sum(v)
    r2=len(v)
    print r2
    m=r1/r2
    std=0
    for i in range(r2):
       std+=pow(abs(v[i]-m),2)  
    res=pow((std/r2),0.5)
    return r1,r2,res

Aqui, o gerador obtém números de um arquivo de texto (de até 15 GB) e aplica matemática simples nesses números usando o redutor de mapa do Hadoop. Se eu não tivesse usado a função yield, mas uma compreensão da lista, levaria muito mais tempo calculando as somas e a média (para não mencionar a complexidade do espaço).

O Hadoop é um ótimo exemplo para usar todas as vantagens dos geradores.

Murphy
fonte