목록 이해는 이해 범위 이후에도 이름을 리 바인드합니다. 이게 옳은 거니?


118

Comprehensions는 범위 지정과 예상치 못한 상호 작용을합니다. 이것이 예상되는 동작입니까?

방법이 있습니다.

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

징징 거리는 위험에 처하면 이것은 잔인한 오류의 원인입니다. 새 코드를 작성할 때 가끔 리 바인딩으로 인해 매우 이상한 오류를 발견합니다. 지금도 문제라는 것을 알고 있습니다. "언제나 밑줄이있는 목록 이해의 임시 변수 머리말"과 같은 규칙을 만들 필요가 있지만, 그렇다고해서 절대적인 것은 아닙니다.

이 임의의 시한 폭탄 대기 종류가 있다는 사실은 목록 이해의 모든 멋진 "사용 편의성"을 무효화합니다.


7
-1 : "잔인한 오류 원인"? 거의. 왜 그런 논증적인 용어를 선택합니까? 일반적으로 가장 비용이 많이 드는 오류는 요구 사항 오해와 단순한 논리 오류입니다. 이러한 종류의 오류는 많은 프로그래밍 언어에서 표준 문제였습니다. 왜 '잔인'이라고 부릅니까?
S.Lott

44
그것은 최소한의 놀라움의 원칙을 위반합니다. 목록 이해력에 대한 파이썬 문서에서도 언급되지 않았지만 얼마나 쉽고 편리한 지 여러 번 언급합니다. 본질적으로 그것은 내 언어 모델 외부에 존재하는 지뢰이기 때문에 내가 예측할 수 없었습니다.
Jabavu Adams

33
"잔인한 오류 소스"의 경우 +1. '잔인한'이라는 단어는 전적으로 정당화됩니다.
Nathaniel 2013

3
여기서 내가 본 유일한 "잔인한"것은 명명 규칙입니다. 이것은 더 이상 80 년대가 아닙니다. 3 개의 문자 변수 이름으로 제한되지 않습니다.
UloPe 2014-06-02

5
참고 : 문서 목록 이해가 명시 적 for루프 구조 및 for루프 누출 변수 와 동일하다고 명시 합니다 . 그래서 그것은 명시 적이 지 않았지만 암시 적으로 명시되었습니다.
Bakuriu

답변:


172

List comprehensions는 Python 2에서 루프 제어 변수를 유출하지만 Python 3에서는 유출하지 않습니다. 다음은 Guido van Rossum (Python의 창시자) 이 그이면의 역사를 설명 합니다.

또한 목록 이해와 생성기 표현식 간의 동등성을 개선하기 위해 Python 3에서 또 다른 변경을했습니다. Python 2에서 목록 이해는 루프 제어 변수를 주변 범위로 "누수"합니다.

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

이것은 목록 이해의 원래 구현의 결과물이었습니다. 그것은 수년간 파이썬의 "더러운 작은 비밀"중 하나였습니다. 목록 이해력을 맹목적으로 빠르게 만들기위한 의도적 인 타협으로 시작되었으며 초보자에게는 일반적인 함정은 아니지만 때때로 사람들을 찌르는 경우가 있습니다. 생성기 표현식의 경우이 작업을 수행 할 수 없습니다. 생성기 표현식은 생성기를 사용하여 구현되며 실행에는 별도의 실행 프레임이 필요합니다. 따라서 생성기 표현식 (특히 짧은 시퀀스를 반복하는 경우)은 목록 이해보다 효율성이 떨어졌습니다.

그러나 Python 3에서는 생성기 표현식과 동일한 구현 전략을 사용하여 목록 이해의 "더티 작은 비밀"을 수정하기로 결정했습니다. 따라서 Python 3에서 위의 예제 (print (x)를 사용하도록 수정 한 후 :-)는 'before'를 인쇄하여 목록 이해의 'x'가 일시적으로 음영 처리되지만 주변의 'x'를 재정의하지 않음을 증명합니다. 범위.


14
나는 Guido가 그것을 "더러운 작은 비밀"이라고 부르지 만 많은 사람들이 그것을 버그가 아니라 기능이라고 생각했다고 덧붙일 것입니다.
Steven Rumbalski 2011

38
또한 이제 2.7에서 집합 및 사전 이해 (및 생성기)에는 개인 범위가 있지만 목록 이해에는 여전히 없습니다. 이것은 전자가 모두 파이썬 3에서 백 포팅되었다는 점에서 어느 정도 의미가 있지만 실제로는 목록 이해와는 대조적입니다.
Matt B.

7
나는 이것이 엄청나게 오래된 질문이라는 것을 알고 있지만 일부 사람들은 그것을 언어의 특징이라고 생각했을까요? 이런 종류의 변수 유출에 유리한 것이 있습니까?
Mathias Müller

2
for : 루프 누수에는 좋은 이유가 있습니다. 조기에 마지막 가치에 접근하기 위해 break-그러나 이해와는 무관합니다. 사람들이 표현 중간에 변수를 할당하기를 원했던 comp.lang.python 토론을 떠 올립니다. 덜 미친 방법이 발견되었습니다 조항 등을위한 단일 값이었다. sum100 = [s for s in [0] for i in range(1, 101) for s in [s + i]][-1],하지만 단지 comprehension-local var가 필요하며 Python 3에서도 잘 작동합니다. "leaking"이 표현식 외부에서 보이는 변수를 설정하는 유일한 방법이라고 생각합니다. 모두가 이러한 기술 :-) 끔찍한 동의
베니 Cherniavsky-Paskin

1
여기서 문제는 목록 이해의 주변 범위에 대한 액세스 권한이 없지만 주변 범위에 영향을 미치는 목록 이해 범위의 바인딩입니다.
Felipe Gonçalves Marques

48

예, for 루프와 마찬가지로 Python 2.x에서 목록 내포물이 변수를 "누출"합니다.

돌이켜 보면 이것은 실수로 인식되었고 생성기 표현식으로 피했습니다. 편집 : Matt B.가 지적 했듯이 Python 3에서 set 및 dictionary 이해 구문을 백 포트 할 때도 피했습니다.

List Comprehensions의 동작은 Python 2에서와 같이 남아 있어야했지만 Python 3에서는 완전히 수정되었습니다.

이는 다음을 의미합니다.

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

x항상 이러한 동안 표현에 로컬 :

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

Python 2.x에서는 모두 x변수를 주변 범위로 유출합니다 .


UPDATE for Python 3.8 (?) : PEP 572의도적 으로 이해와 생성기 표현식에서 유출 되는 :=할당 연산자를 도입 합니다 ! 본질적으로 두 가지 사용 사례에 의해 동기가 부여됩니다 : 및 다음 과 같은 조기 종료 기능에서 "증인"캡처 .any()all()

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

변경 가능한 상태 업데이트 :

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

정확한 범위 지정 은 부록 B 를 참조하십시오 . 변수는 주변의 가장 가까운에 할당 def하거나 lambda그 기능을 선언하지 않는 한, nonlocal또는 global.


7

예, for루프 에서와 마찬가지로 할당이 발생합니다 . 새 범위가 생성되지 않습니다.

이것은 확실히 예상되는 동작입니다. 각주기에서 값은 지정한 이름에 바인딩됩니다. 예를 들어

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

일단 그것이 인식되면 피하는 것이 충분히 쉬워 보입니다. 이해 범위 내에서 변수에 대해 기존 이름을 사용하지 마십시오.


2

흥미롭게도 이것은 사전이나 집합 이해력에 영향을 미치지 않습니다.

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

그러나 위에서 언급 한대로 3에서 수정되었습니다.


이 구문은 Python 2.6에서 전혀 작동하지 않습니다. Python 2.7에 대해 이야기하고 있습니까?
Paul Hollingsworth

Python 2.6에는 Python 3.0과 마찬가지로 목록 이해 기능 만 있습니다. 3.1은 집합 및 사전 이해를 추가했으며 이들은 2.7로 이식되었습니다. 명확하지 않다면 죄송합니다. 다른 답변에 대한 제한 사항을 지적하기위한 것이며 적용되는 버전은 완전히 간단하지 않습니다.
Chris Travers 2017 년

새 코드에 python 2.7을 사용하는 것이 합리적이라는 주장을 할 수는 있지만, python 2.6에 대해 똑같이 말할 수는 없습니다 ... 2.6이 OS와 함께 제공 되더라도, 그것. virtualenv를 설치하고 새 코드에 3.6을 사용하는 것을 고려하십시오!
Alex L

파이썬 2.6에 대한 요점은 기존의 레거시 시스템을 유지하는 데서 올 수 있습니다. 따라서 역사적 기록으로서 그것은 완전히 무관하지 않습니다. 3.0 (ick)과 동일
Chris Travers 2017

무례하게 들리면 미안하지만 어떤 식 으로든 질문에 답할 수 없습니다. 댓글로 더 적합합니다.
0xc0de

1

이 동작이 바람직하지 않은 경우 Python 2.6의 일부 해결 방법

# 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

-1

목록 이해 중에 python3에서 변수는 범위가 끝난 후에도 변경되지 않지만 간단한 for 루프를 사용하면 변수가 범위를 벗어나 다시 할당됩니다.

i = 1 print (i) print ([i in range (5)]) print (i) i의 값은 1로만 유지됩니다.

이제 단순히 for 루프를 사용하면 i의 값이 다시 할당됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.