Zrozumienie listy ponownie wiąże nazwy nawet po zakresie zrozumienia. Czy to jest poprawne?

118

Zrozumienia mają nieoczekiwane interakcje z określaniem zakresu. Czy to jest oczekiwane zachowanie?

Mam metodę:

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

Ryzykując marudzenie, jest to brutalne źródło błędów. Kiedy piszę nowy kod, od czasu do czasu znajduję bardzo dziwne błędy spowodowane ponownym wiązaniem - nawet teraz, gdy wiem, że to problem. Muszę utworzyć regułę typu „zawsze poprzedzać zmienne tymczasowe w listach składanych z podkreśleniem”, ale nawet to nie jest niezawodne.

Fakt, że czeka na nas ta losowa bomba zegarowa, w pewnym sensie neguje całą przyjemną „łatwość użycia” składania list.

Jabavu Adams
źródło
7
-1: „brutalne źródło błędów”? Ledwie. Dlaczego warto wybrać taki argumentujący termin? Generalnie najdroższymi błędami są nieporozumienia wymagań i proste błędy logiczne. Ten rodzaj błędu był standardowym problemem w wielu językach programowania. Dlaczego nazywa się to „brutalnym”?
S.Lott,
44
Narusza zasadę najmniejszego zaskoczenia. Nie ma o tym również wzmianki w dokumentacji Pythona na temat składanych list, która jednak kilkakrotnie wspomina, jak łatwe i wygodne są one. Zasadniczo jest to mina lądowa, która istniała poza moim modelem językowym i dlatego była niemożliwa do przewidzenia.
Jabavu Adams
33
+1 za „brutalne źródło błędów”. Słowo „brutalny” jest w pełni uzasadnione.
Nathaniel,
3
Jedyną „brutalną” rzeczą, jaką tu widzę, jest twoja konwencja nazywania. To już nie są lata 80., nie jesteś już ograniczony do 3-znakowych nazw zmiennych.
UloPe
5
Uwaga: dokumentację, czy stan, który lista-rozumienie są równoważne wyraźnej for-loop konstruktem i for-loops zmienne wycieków . Nie było to więc wyraźne, ale zostało to stwierdzone w sposób dorozumiany.
Bakuriu,

Odpowiedzi:

172

Listy składane powodują wyciek zmiennej sterującej pętli w Pythonie 2, ale nie w Pythonie 3. Oto Guido van Rossum (twórca Pythona) wyjaśniający historię tego:

Wprowadziliśmy również kolejną zmianę w Pythonie 3, aby poprawić równoważność między składnikami list i wyrażeniami generatora. W Pythonie 2, lista składana „przecieka” zmienną sterującą pętli do otaczającego zakresu:

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

Był to artefakt pierwotnej implementacji list składanych; był to jeden z „brudnych małych sekretów” Pythona przez lata. Zaczęło się jako celowy kompromis, mający na celu oślepiająco szybkie tworzenie list, i chociaż nie było to częstą pułapką dla początkujących, od czasu do czasu kłuło ludzi. W przypadku wyrażeń generatora nie mogliśmy tego zrobić. Wyrażenia generatora są implementowane przy użyciu generatorów, których wykonanie wymaga oddzielnej ramki wykonania. Zatem wyrażenia generatora (zwłaszcza jeśli iterują po krótkiej sekwencji) były mniej wydajne niż wyrażenia listowe.

Jednak w Pythonie 3 zdecydowaliśmy się naprawić „brudny mały sekret” składanych list, używając tej samej strategii implementacji, co w przypadku wyrażeń generatora. Tak więc w Pythonie 3 powyższy przykład (po modyfikacji w celu użycia print (x) :-) wypisze „przed”, udowadniając, że „x” w liście składowanym tymczasowo zacienia się, ale nie zastępuje „x” w otoczeniu zakres.

Steven Rumbalski
źródło
14
Dodam, że chociaż Guido nazywa to „brudnym małym sekretem”, wielu uważało to za cechę, a nie błąd.
Steven Rumbalski
38
Zauważ również, że teraz w wersji 2.7 zestawy i wyrażenia słownikowe (i generatory) mają prywatne zakresy, ale listy nadal nie mają. Chociaż ma to jakiś sens, ponieważ wszystkie te pierwsze zostały przeniesione z Pythona 3, to naprawdę sprawia, że ​​kontrast ze składaniem list jest irytujący.
Matt B.
7
Wiem, że to szalenie stare pytanie, ale dlaczego niektórzy uważali je za cechę języka? Czy jest coś na korzyść tego rodzaju wycieku zmiennych?
Mathias Müller
2
za: przeciekanie pętli ma dobre powody, zwł. aby uzyskać dostęp do ostatniej wartości po wczesnym break- ale nie ma znaczenia dla zrozumienia. Przypominam sobie dyskusje o comp.lang.python, w których ludzie chcieli przypisać zmienne w środku wyrażenia. Mniej szalony sposób, że było jedno-wartość dla klauzule np. sum100 = [s for s in [0] for i in range(1, 101) for s in [s + i]][-1], ale potrzebuje tylko zmiennej lokalnej dla zrozumienia i działa równie dobrze w Pythonie 3. Myślę, że „wyciek” był jedynym sposobem na ustawienie zmiennej widocznej poza wyrażeniem. Wszyscy zgodzili się, że te techniki są okropne :-)
Beni Cherniavsky-Paskin
1
Problem polega na tym, że nie ma dostępu do otaczającego zakresu wyrażeń listowych, ale wiązanie w zakresie list składanych wpływa na otaczający zakres.
Felipe Gonçalves Marques
48

Tak, wyrażenia listowe „przeciekają” swoją zmienną w Pythonie 2.x, podobnie jak pętle for.

Z perspektywy czasu uznano to za błąd i uniknięto go dzięki wyrażeniom generatora. EDYCJA: Jak zauważa Matt B., uniknięto tego również, gdy składnie zestawu i słownika zostały przeniesione z Pythona 3.

Zachowanie list składanych musiało pozostać takie, jak w Pythonie 2, ale zostało w pełni naprawione w Pythonie 3.

Oznacza to, że we wszystkich:

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

xjest zawsze lokalne dla wyrażenia podczas nich:

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

w Pythonie 2.x wszystkie przeciekają xzmienną do otaczającego zakresu.


UPDATE for Python 3.8 (?) : PEP 572 wprowadzi :=operator przypisania, który celowo wycieka ze zrozumień i wyrażeń generatora! Motywują go zasadniczo 2 przypadki użycia: przechwytywanie „świadka” funkcji wczesnego kończenia, takich jak any()i all():

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

i aktualizowanie stanu mutowalnego:

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

Patrz Dodatek B do dokładnego określania zakresu. Zmienna jest przypisywana w najbliższym otoczeniu deflub lambda, chyba że funkcja deklaruje to nonlocallub global.

Beni Cherniavsky-Paskin
źródło
7

Tak, przypisanie następuje tam, tak jak w forpętli. Nie jest tworzony żaden nowy zakres.

Jest to zdecydowanie oczekiwane zachowanie: w każdym cyklu wartość jest przypisywana podanej nazwie. Na przykład,

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

Gdy zostanie to rozpoznane, wydaje się łatwe do uniknięcia: nie używaj istniejących nazw dla zmiennych w ramach pojmowania.

JAL
źródło
2

Co ciekawe, nie ma to wpływu na słownik ani zestawy pojęć.

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

Jednak zostało to naprawione w 3, jak wspomniano powyżej.

Chris Travers
źródło
Ta składnia w ogóle nie działa w Pythonie 2.6. Mówisz o Pythonie 2.7?
Paul Hollingsworth,
Python 2.6 obsługuje tylko listy składane, podobnie jak Python 3.0. 3.1 dodał zestawy i wyrażenia słownikowe, które zostały przeniesione do wersji 2.7. Przepraszam, jeśli to nie było jasne. Miało to na celu zwrócenie uwagi na ograniczenie innej odpowiedzi, a do których wersji się odnosi, nie jest do końca proste.
Chris Travers,
Chociaż mogę sobie wyobrazić argumentację, że są przypadki, w których używanie Pythona 2.7 do nowego kodu ma sens, nie mogę powiedzieć tego samego o Pythonie 2.6 ... Nawet jeśli 2.6 jest tym, co jest dostarczane z systemem operacyjnym, nie utkniesz z to. Rozważ zainstalowanie virtualenv i użycie 3.6 dla nowego kodu!
Alex L
Kwestia dotycząca Pythona 2.6 może pojawić się jednak przy utrzymaniu istniejących starszych systemów. Tak więc jako notatka historyczna nie jest to całkowicie bez znaczenia. To samo z 3.0 (ick)
Chris Travers
Przepraszam, jeśli brzmię niegrzecznie, ale to w żaden sposób nie odpowiada na pytanie. Lepiej pasuje jako komentarz.
0xc0de
1

pewne obejście dla Pythona 2.6, gdy takie zachowanie nie jest pożądane

# 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
źródło
-1

W python3, gdy jest w trybie listowym, zmienna nie zmienia się po zakończeniu jej zakresu, ale kiedy używamy prostej pętli for, zmienna zostaje ponownie przypisana poza zakres.

i = 1 print (i) print ([i w zakresie (5)]) print (i) Wartość i pozostanie tylko 1.

Teraz po prostu użyj pętli for, a wartość i zostanie ponownie przypisana.

ASHOK KUMAR
źródło