A compreensão de lista vincula nomes mesmo após o escopo de compreensão. Isto está certo?

118

As compreensões estão tendo algumas interações inesperadas com o escopo. Este é o comportamento esperado?

Eu tenho um método:

def leave_room(self, uid):
  u = self.user_by_id(uid)
  r = self.rooms[u.rid]

  other_uids = [ouid for ouid in r.users_by_id.keys() if ouid != u.uid]
  other_us = [self.user_by_id(uid) for uid in other_uids]

  r.remove_user(uid) # OOPS! uid has been re-bound by the list comprehension above

  # Interestingly, it's rebound to the last uid in the list, so the error only shows
  # up when len > 1

Correndo o risco de choramingar, esta é uma fonte brutal de erros. Enquanto escrevo um novo código, ocasionalmente encontro erros muito estranhos devido à religação - mesmo agora que sei que é um problema. Eu preciso criar uma regra como "sempre prefacie variáveis ​​temporárias em compreensões de lista com sublinhado", mas mesmo isso não é à prova de idiotas.

O fato de haver essa bomba-relógio aleatória esperando nega toda a boa "facilidade de uso" da compreensão de listas.

Jabavu Adams
fonte
7
-1: "fonte brutal de erros"? Dificilmente. Por que escolher esse termo argumentativo? Geralmente, os erros mais caros são mal-entendidos de requisitos e erros de lógica simples. Esse tipo de erro tem sido um problema padrão em muitas linguagens de programação. Por que chamá-lo de 'brutal'?
S.Lott
44
Isso viola o princípio da menor surpresa. Também não é mencionado na documentação do python sobre compreensões de listas que, entretanto, menciona várias vezes como são fáceis e convenientes. Essencialmente, é uma mina terrestre que existia fora do meu modelo de linguagem e, portanto, era impossível para mim prever.
Jabavu Adams
33
1 para "fonte brutal de erros". A palavra 'brutal' é inteiramente justificada.
Nathaniel de
3
A única coisa "brutal" que vejo aqui é sua convenção de nomenclatura. Este não é mais os anos 80, você não está limitado a nomes de variáveis ​​de 3 caracteres.
UloPe
5
Nota: o documention faz estado que lista-compreensão são equivalentes ao explícita forconstrução -loop e for-loops variáveis de vazamento . Portanto, não foi explícito, mas foi declarado implicitamente.
Bakuriu

Respostas:

172

Compreensões de lista vazam a variável de controle de loop no Python 2, mas não no Python 3. Aqui está Guido van Rossum (criador do Python) explicando a história por trás disso:

Também fizemos outra mudança no Python 3, para melhorar a equivalência entre compreensões de lista e expressões geradoras. No Python 2, a compreensão de lista "vaza" a variável de controle de loop para o escopo circundante:

x = 'before'
a = [x for x in 1, 2, 3]
print x # this prints '3', not 'before'

Este foi um artefato da implementação original de compreensões de lista; foi um dos "segredinhos sujos" do Python durante anos. Começou como um compromisso intencional para tornar a compreensão de listas incrivelmente rápida e, embora não fosse uma armadilha comum para iniciantes, definitivamente feriu as pessoas ocasionalmente. Para expressões geradoras, não poderíamos fazer isso. Expressões de gerador são implementadas usando geradores, cuja execução requer um quadro de execução separado. Assim, as expressões geradoras (especialmente se iterarem em uma sequência curta) foram menos eficientes do que as compreensões de lista.

No entanto, no Python 3, decidimos consertar o "segredinho sujo" das compreensões de lista usando a mesma estratégia de implementação das expressões geradoras. Assim, no Python 3, o exemplo acima (após a modificação para usar print (x) :-) imprimirá 'antes', provando que o 'x' na compreensão da lista escurece temporariamente, mas não sobrescreve o 'x' ao redor escopo.

Steven Rumbalski
fonte
14
Vou acrescentar que, embora Guido o chame de "segredinho sujo", muitos o consideraram um recurso, não um bug.
Steven Rumbalski de
38
Observe também que agora no 2.7, as compreensões de conjunto e dicionário (e geradores) têm escopos privados, mas as compreensões de lista ainda não. Embora isso faça algum sentido, já que os primeiros foram todos portados do Python 3, realmente torna o contraste com as compreensões de lista chocante.
Matt B.
7
Eu sei que esta é uma pergunta insanamente antiga, mas por que alguns consideram isso uma característica da linguagem? Existe algo a favor desse tipo de vazamento variável?
Mathias Müller
2
para: o vazamento de loops tem boas razões, esp. para acessar o último valor depois do início break- mas irrelevante para comprehesions. Lembro-me de algumas discussões em comp.lang.python em que as pessoas queriam atribuir variáveis ​​no meio da expressão. A forma menos insana encontrada foi de valor único para as cláusulas, por exemplo. sum100 = [s for s in [0] for i in range(1, 101) for s in [s + i]][-1], mas só precisa de uma var local de compreensão e funciona tão bem no Python 3. Acho que "vazar" era a única maneira de definir a variável visível fora de uma expressão. Todos concordaram que essas técnicas são horríveis :-)
Beni Cherniavsky-Paskin
1
O problema aqui não é ter acesso ao escopo circundante das compreensões da lista, mas vincular o escopo das compreensões da lista afetando o escopo circundante.
Felipe Gonçalves Marques
48

Sim, as compreensões de lista "vazam" suas variáveis ​​no Python 2.x, assim como os loops for.

Em retrospecto, isso foi reconhecido como um erro e foi evitado com expressões geradoras. EDIT: Como Matt B. observa , também foi evitado quando as sintaxes de compreensão de conjunto e dicionário foram portadas do Python 3.

O comportamento das compreensões de lista teve que ser deixado como está no Python 2, mas está totalmente corrigido no Python 3.

Isso significa que em todos:

list(x for x in a if x>32)
set(x//4 for x in a if x>32)         # just another generator exp.
dict((x, x//16) for x in a if x>32)  # yet another generator exp.
{x//4 for x in a if x>32}            # 2.7+ syntax
{x: x//16 for x in a if x>32}        # 2.7+ syntax

o xé sempre local para a expressão enquanto estes:

[x for x in a if x>32]
set([x//4 for x in a if x>32])         # just another list comp.
dict([(x, x//16) for x in a if x>32])  # yet another list comp.

no Python 2.x, todos vazam a xvariável para o escopo circundante.


ATUALIZAÇÃO para Python 3.8 (?) : PEP 572 introduzirá o :=operador de atribuição que vaza deliberadamente de compreensões e expressões geradoras! É motivado por essencialmente 2 casos de uso: capturar uma "testemunha" de funções de encerramento antecipado como any()e all():

if any((comment := line).startswith('#') for line in lines):
    print("First comment:", comment)
else:
    print("There are no comments")

e atualizando o estado mutável:

total = 0
partial_sums = [total := total + v for v in values]

Consulte o Apêndice B para obter o escopo exato. A variável é atribuída nos arredores mais próximos defou lambda, a menos que a função declare nonlocalou global.

Beni Cherniavsky-Paskin
fonte
7

Sim, a atribuição ocorre lá, exatamente como em um forloop. Nenhum novo escopo está sendo criado.

Este é definitivamente o comportamento esperado: em cada ciclo, o valor é vinculado ao nome que você especificar. Por exemplo,

>>> x=0
>>> a=[1,54,4,2,32,234,5234,]
>>> [x for x in a if x>32]
[54, 234, 5234]
>>> x
5234

Depois que isso for reconhecido, parece fácil de evitar: não use nomes existentes para as variáveis ​​dentro das compreensões.

JAL
fonte
2

Curiosamente, isso não afeta o dicionário ou a compreensão do conjunto.

>>> [x for x in range(1, 10)]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> x
9
>>> {x for x in range(1, 5)}
set([1, 2, 3, 4])
>>> x
9
>>> {x:x for x in range(1, 100)}
{1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12, 13: 13, 14: 14, 15: 15, 16: 16, 17: 17, 18: 18, 19: 19, 20: 20, 21: 21, 22: 22, 23: 23, 24: 24, 25: 25, 26: 26, 27: 27, 28: 28, 29: 29, 30: 30, 31: 31, 32: 32, 33: 33, 34: 34, 35: 35, 36: 36, 37: 37, 38: 38, 39: 39, 40: 40, 41: 41, 42: 42, 43: 43, 44: 44, 45: 45, 46: 46, 47: 47, 48: 48, 49: 49, 50: 50, 51: 51, 52: 52, 53: 53, 54: 54, 55: 55, 56: 56, 57: 57, 58: 58, 59: 59, 60: 60, 61: 61, 62: 62, 63: 63, 64: 64, 65: 65, 66: 66, 67: 67, 68: 68, 69: 69, 70: 70, 71: 71, 72: 72, 73: 73, 74: 74, 75: 75, 76: 76, 77: 77, 78: 78, 79: 79, 80: 80, 81: 81, 82: 82, 83: 83, 84: 84, 85: 85, 86: 86, 87: 87, 88: 88, 89: 89, 90: 90, 91: 91, 92: 92, 93: 93, 94: 94, 95: 95, 96: 96, 97: 97, 98: 98, 99: 99}
>>> x
9

No entanto, foi corrigido em 3 conforme observado acima.

Chris Travers
fonte
Essa sintaxe não funciona em Python 2.6. Você está falando sobre Python 2.7?
Paul Hollingsworth de
O Python 2.6 tem compreensões de lista apenas como o Python 3.0. 3.1 adicionou conjuntos e compreensões de dicionário e estes foram transferidos para 2.7. Desculpe se isso não ficou claro. O objetivo era observar uma limitação para outra resposta, e as versões às quais se aplica não são totalmente diretas.
Chris Travers de
Embora eu possa imaginar argumentar que há casos em que usar o python 2.7 para um novo código faz sentido, não posso dizer o mesmo para o python 2.6 ... Mesmo que 2.6 seja o que veio com o seu sistema operacional, você não está preso isto. Considere instalar o virtualenv e usar o 3.6 para o novo código!
Alex L
A questão sobre o Python 2.6 pode surgir na manutenção de sistemas legados existentes. Portanto, como nota histórica, não é totalmente irrelevante. Mesmo com 3.0 (ick)
Chris Travers
Desculpe se pareço rude, mas isso não responde à pergunta de forma alguma. É mais adequado como comentário.
0xc0de
1

alguma solução alternativa, para python 2.6, quando esse comportamento não é desejável

# python
Python 2.6.6 (r266:84292, Aug  9 2016, 06:11:56)
Type "help", "copyright", "credits" or "license" for more information.
>>> x=0
>>> a=list(x for x in xrange(9))
>>> x
0
>>> a=[x for x in xrange(9)]
>>> x
8
Marek Slebodnik
fonte
-1

Em python3, enquanto em compreensão de lista, a variável não obtém mudança após seu escopo terminar, mas quando usamos o loop for simples, a variável é reatribuída fora do escopo.

i = 1 print (i) print ([i no intervalo (5)]) print (i) O valor de i permanecerá 1 apenas.

Agora basta usar simplesmente o loop for, o valor de i será reatribuído.

ASHOK KUMAR
fonte