PEP 572 – 대입 표현식
- Author:
- Chris Angelico <rosuav at gmail.com>, Tim Peters <tim.peters at gmail.com>, Guido van Rossum <guido at python.org>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 28-Feb-2018
- Python-Version:
- 3.8
- Post-History:
- 28-Feb-2018, 02-Mar-2018, 23-Mar-2018, 04-Apr-2018, 17-Apr-2018, 25-Apr-2018, 09-Jul-2018, 05-Aug-2019
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 문서는 NAME := expr 표기법을 사용하여 표현식 안에서 변수에 값을 대입하는 방법을 만들자는 제안입니다.
이 변경의 일부로 딕셔너리 컴프리헨션의 평가 순서도 갱신하여, 키 표현식이 값 표현식보다 먼저 실행되도록 보장합니다(이로써 키를 이름에 바인딩한 다음 대응하는 값을 계산할 때 재사용할 수 있습니다).
이 PEP를 논의하는 동안 이 연산자는 비공식적으로 “바다코끼리 연산자(the walrus operator)”로 알려지게 되었습니다. 이 구문의 공식 명칭은 (PEP 제목에 따라) “대입 표현식(Assignment Expressions)”이지만, “이름 있는 표현식(Named Expressions)”이라고 부르기도 합니다(예를 들어 CPython 참조 구현은 내부적으로 이 이름을 사용합니다).
근거
표현식의 결과에 이름을 붙이는 것은 프로그래밍의 중요한 부분이며, 더 긴 표현식 대신 설명적인 이름을 사용할 수 있게 하고 재사용도 가능하게 합니다. 현재 이 기능은 문(statement) 형태로만 사용할 수 있어서, 리스트 컴프리헨션이나 다른 표현식 문맥에서는 사용할 수 없습니다.
또한 큰 표현식의 하위 부분에 이름을 붙이면 대화형 디버거에 도움이 되며, 유용한 표시 훅과 부분 결과를 제공할 수 있습니다. 하위 표현식을 인라인으로 포착할 방법이 없으면 원래 코드를 리팩터링해야 하지만, 대입 표현식을 사용하면 name := 표시를 몇 개 삽입하기만 하면 됩니다. 리팩터링할 필요가 없어지면 디버깅 과정에서 코드가 의도치 않게 변경될 가능성(하이젠버그의 흔한 원인)이 줄어들고, 다른 프로그래머에게 구두로 전달하기도 더 쉽습니다.
실제 코드의 중요성
이 PEP를 개발하는 동안 많은 사람들(지지자와 비판자 모두)이 한편으로는 장난감 같은 예제에, 다른 한편으로는 지나치게 복잡한 예제에 초점을 맞추는 경향을 보였습니다.
장난감 같은 예제의 위험은 두 가지입니다. 너무 추상적이어서 누구도 “오, 설득력 있네”라고 느끼게 만들지 못하는 경우가 많고, “어차피 나라면 절대 그렇게 작성하지 않을 것”이라는 말로 쉽게 반박됩니다.
지나치게 복잡한 예제의 위험은 제안을 비판하는 사람들이 쉽게 공격할 수 있는 편리한 허수아비를 제공한다는 점입니다(“이건 난독화된 코드입니다”).
그래도 극도로 단순한 예제와 극도로 복잡한 예제 모두 어느 정도 쓸모가 있습니다. 의도한 의미를 명확히 하는 데 도움이 되기 때문입니다. 따라서 아래에는 두 종류의 예제가 조금씩 포함되어 있습니다.
하지만 설득력을 갖추려면 예제는 실제 코드에 뿌리를 두어야 합니다. 즉, 규모가 크든 작든 유용한 애플리케이션의 일부로서 이 PEP를 전혀 염두에 두지 않고 작성된 코드여야 합니다. Tim Peters는 자신의 개인 코드 저장소를 살펴보고, 자신이 작성한 코드 중에서 (그의 관점에서) 대입 표현식을 (절제하여) 사용해 다시 작성하면 더 명확해졌을 예제들을 골라 주어 매우 큰 도움을 주었습니다. 그의 결론은 현재 제안이 상당히 많은 코드 조각에서 크지는 않지만 분명한 개선을 가능하게 했을 것이라는 점입니다.
실제 코드의 또 다른 용도는 프로그래머들이 간결함을 얼마나 중요하게 여기는지 간접적으로 관찰하는 것입니다. Guido van Rossum은 Dropbox 코드 베이스를 검색하여, 프로그래머들이 줄을 짧게 쓰는 것보다 줄 수를 적게 쓰는 것을 더 중요하게 여긴다는 몇 가지 증거를 발견했습니다.
대표적인 사례로, Guido는 프로그래머가 코드 한 줄을 줄이기 위해 프로그램을 느리게 만들면서까지 하위 표현식을 반복한 예제를 여러 개 발견했습니다. 예를 들어 다음과 같이 작성하는 대신:
match = re.match(data)
group = match.group(1) if match else None
다음과 같이 작성했습니다:
group = re.match(data).group(1) if re.match(data) else None
또 다른 예제는 프로그래머들이 들여쓰기 한 단계를 줄이기 위해 때로 더 많은 작업을 한다는 것을 보여 줍니다:
match1 = pattern1.match(data)
match2 = pattern2.match(data)
if match1:
result = match1.group(1)
elif match2:
result = match2.group(2)
else:
result = None
이 코드는 pattern1이 일치하는 경우에도 pattern2와의 일치를 시도합니다(이 경우 pattern2의 일치 결과는 전혀 사용되지 않습니다). 더 효율적으로 다시 작성하면 다음과 같았을 것입니다:
match1 = pattern1.match(data)
if match1:
result = match1.group(1)
else:
match2 = pattern2.match(data)
if match2:
result = match2.group(2)
else:
result = None
문법과 의미
임의의 Python 표현식을 사용할 수 있는 대부분의 문맥에서 이름 있는 표현식이 나타날 수 있습니다. 이는 NAME := expr 형태이며, 여기서 expr은 괄호로 감싸지 않은 튜플을 제외한 모든 유효한 Python 표현식이고, NAME은 식별자입니다.
이러한 이름 있는 표현식의 값은 포함된 표현식의 값과 같으며, 대상에 그 값이 대입된다는 부수 효과가 추가로 발생합니다:
# Handle a matched regex
if (match := pattern.search(data)) is not None:
# Do something with match
# A loop that can't be trivially rewritten using 2-arg iter()
while chunk := file.read(8192):
process(chunk)
# Reuse a value that's expensive to compute
[y := f(x), y**2, y**3]
# Share a subexpression between a comprehension filter clause and its output
filtered_data = [y for x in data if (y := f(x)) is not None]
예외적인 경우
모호함이나 사용자의 혼란을 피하기 위해 대입 표현식이 허용되지 않는 곳이 몇 군데 있습니다:
- 괄호로 감싸지 않은 대입 표현식은 표현식 문의 최상위 수준에서 금지됩니다. 예시:
y := f(x) # INVALID (y := f(x)) # Valid, though not recommended
이 규칙은 사용자가 대입 문과 대입 표현식 중 하나를 더 쉽게 고를 수 있도록 하기 위해 포함되었습니다. 둘 다 유효한 구문상의 위치는 없습니다.
- 괄호로 감싸지 않은 대입 표현식은 대입 문 우변의 최상위 수준에서 금지됩니다. 예시:
y0 = y1 := f(x) # INVALID y0 = (y1 := f(x)) # Valid, though discouraged
이 규칙 역시 같은 것을 표현하는 시각적으로 비슷한 두 가지 방식을 피하기 위해 포함되었습니다.
- 괄호로 감싸지 않은 대입 표현식은 호출에서 키워드 인자의 값으로 사용할 수 없습니다. 예시:
foo(x = y := f(x)) # INVALID foo(x=(y := f(x))) # Valid, though probably confusing
이 규칙은 지나치게 혼란스러운 코드를 막기 위해, 그리고 키워드 인자의 파싱이 이미 충분히 복잡하기 때문에 포함되었습니다.
- 괄호로 감싸지 않은 대입 표현식은 함수 기본값의 최상위 수준에서 금지됩니다. 예시:
def foo(answer = p := 42): # INVALID ... def foo(answer=(p := 42)): # Valid, though not great style ...
이 규칙은 정확한 의미가 이미 많은 사용자에게 혼란스러운 위치에서 부작용을 억제하기 위해(가변 기본값을 피하라는 일반적인 스타일 권고 참고), 그리고 호출에서의 비슷한 금지(앞 항목)와 맥을 같이하기 위해 포함되었습니다.
- 괄호로 감싸지 않은 대입 표현식은 인자, 반환값, 대입의 어노테이션으로 사용할 수 없습니다. 예시:
def foo(answer: p := 42 = 5): # INVALID ... def foo(answer: (p := 42) = 5): # Valid, but probably never useful ...
여기서의 논리는 앞의 두 경우와 비슷합니다.
:와=로 이루어진, 묶이지 않은 기호와 연산자의 뒤섞임은 올바르게 읽기 어렵습니다. - 괄호로 감싸지 않은 대입 표현식은 람다 함수에서 금지됩니다. 예시:
(lambda: x := 1) # INVALID lambda: (x := 1) # Valid, but unlikely to be useful (x := lambda: 1) # Valid lambda line: (m := re.match(pattern, line)) and m.group(1) # Valid
이를 통해
lambda는 항상:=보다 느슨하게 결합됩니다. 람다 함수 내부의 최상위 수준에서 이름을 바인딩하더라도 그것을 활용할 방법이 없으므로 가치가 있을 가능성은 낮습니다. 이름이 두 번 이상 사용되는 경우에는 어차피 표현식을 괄호로 감싸야 할 가능성이 높으므로, 이 금지 규칙이 코드에 영향을 미치는 일은 드물 것입니다. - f-문자열 내부의 대입 표현식에는 괄호가 필요합니다. 예시:
>>> f'{(x:=10)}' # Valid, uses assignment expression '10' >>> x = 10 >>> f'{x:=10}' # Valid, passes '=10' to formatter ' 10'
이는 f-문자열에서 대입 연산자처럼 보이는 것이 항상 대입 연산자는 아니라는 점을 보여 줍니다. f-문자열 파서는 서식 옵션을 나타내기 위해
:를 사용합니다. 하위 호환성을 유지하기 위해, f-문자열 내부에서 대입 연산자를 사용할 때는 반드시 괄호로 감싸야 합니다. 앞에서 언급했듯이, 이러한 대입 연산자 사용은 권장되지 않습니다.
대상의 스코프
대입 표현식은 새로운 스코프를 만들지 않습니다. 대부분의 경우 대상이 바인딩될 스코프는 자명합니다. 바로 현재 스코프입니다. 이 스코프에 대상에 대한 nonlocal 또는 global 선언이 있으면, 대입 표현식은 그 선언을 따릅니다. 람다는 (익명이긴 하지만 명시적인 함수 정의이므로) 이 목적에서 하나의 스코프로 간주됩니다.
한 가지 특별한 경우가 있습니다. 리스트, 집합 또는 딕셔너리 컴프리헨션이나 제너레이터 표현식(아래에서는 통틀어 “컴프리헨션”이라고 합니다)에 나타나는 대입 표현식은 대상을 둘러싸는 스코프에 바인딩하며, 그 스코프에 대상에 대한 nonlocal 또는 global 선언이 있으면 그 선언을 따릅니다. 이 규칙에서 중첩된 컴프리헨션을 둘러싸는 스코프는 가장 바깥쪽 컴프리헨션을 포함하는 스코프입니다. 람다도 둘러싸는 스코프로 간주합니다.
이 특별한 경우를 둔 동기는 두 가지입니다. 첫째, any() 표현식에 대한 “증거(witness)”나 all()에 대한 반례를 편리하게 포착할 수 있습니다. 예를 들면 다음과 같습니다.:
if any((comment := line).startswith('#') for line in lines):
print("First comment:", comment)
else:
print("There are no comments")
if all((nonblank := line).strip() == '' for line in lines):
print("All lines are blank")
else:
print("First non-blank line:", nonblank)
둘째, 컴프리헨션에서 가변 상태를 간결하게 갱신할 수 있습니다. 예를 들면 다음과 같습니다.:
# Compute partial sums in a list comprehension
total = 0
partial_sums = [total := total + v for v in values]
print("Total:", total)
그러나 대입 표현식의 대상 이름은 그 대입 표현식을 포함하는 어떤 컴프리헨션에 나타나는 for 대상 이름과도 같을 수 없습니다. 후자의 이름들은 자신이 나타나는 컴프리헨션의 지역 이름이므로, 그 안에서 같은 이름을 사용했을 때 대신 가장 바깥쪽 컴프리헨션을 포함하는 스코프를 가리키게 된다면 모순이 됩니다.
예를 들어 [i := i+1 for i in range(5)]는 유효하지 않습니다. for i 부분은 i가 컴프리헨션의 지역 이름임을 확립하지만, i := 부분은 i가 컴프리헨션의 지역 이름이 아니라고 주장하기 때문입니다. 같은 이유로 다음 예들도 유효하지 않습니다.:
[[(j := j) for i in range(5)] for j in range(5)] # INVALID
[i := 0 for i, j in stuff] # INVALID
[i+1 for i in (i := stuff)] # INVALID
이러한 경우에 일관된 의미를 부여하는 것이 기술적으로는 가능하지만, 실제 사용 사례가 없는 상황에서는 그 의미가 정말로 타당한지 판단하기 어렵습니다. 따라서 참조 구현 [1]은 이러한 경우를 구현에 따라 정의되는 동작으로 실행하지 않고 SyntaxError를 발생시키도록 합니다.
이 제한은 대입 표현식이 한 번도 실행되지 않더라도 적용됩니다.:
[False and (i := 0) for i, j in stuff] # INVALID
[i for i, j in stuff if True or (j := 1)] # INVALID
컴프리헨션 본문(첫 번째 “for” 키워드 앞부분)과 필터 표현식(“if” 뒤, 중첩된 “for” 앞부분)에서는 컴프리헨션의 반복 변수로도 사용되는 대상 이름에만 이 제한이 적용됩니다. 이러한 위치에 나타나는 람다 표현식은 새로운 명시적 함수 스코프를 도입하므로, 추가 제한 없이 대입 표현식을 사용할 수 있습니다.
참조 구현의 설계상 제약(심볼 테이블 분석기는 가장 왼쪽 컴프리헨션 이터러블 표현식과 컴프리헨션의 나머지 부분 사이에서 이름이 재사용되는 경우를 쉽게 감지할 수 없습니다) 때문에, 컴프리헨션 이터러블 표현식(각 “in” 뒤, 그다음 “if” 또는 “for” 키워드 앞부분)의 일부로는 이름 있는 표현식을 전혀 사용할 수 없습니다.:
[i+1 for i in (j := stuff)] # INVALID
[i+1 for i in range(2) for j in (k := stuff)] # INVALID
[i+1 for i in [j for j in (k := stuff)]] # INVALID
[i+1 for i in (lambda: (j := stuff))()] # INVALID
대입 표현식이 클래스 스코프를 둘러싸는 스코프로 가진 컴프리헨션에 나타나는 경우에는 예외가 하나 더 적용됩니다. 위 규칙에 따라 대상이 그 클래스의 스코프에 대입되는 결과가 나온다면, 그 대입 표현식은 명시적으로 유효하지 않습니다. 이 경우에도 SyntaxError가 발생합니다.:
class Example:
[(j := i) for i in range(5)] # INVALID
(후자의 예외를 둔 이유는 컴프리헨션에 대해 암묵적인 함수 스코프가 만들어지기 때문입니다. 현재는 함수가 둘러싸는 클래스 스코프의 변수를 참조할 수 있는 런타임 메커니즘이 없으며, 우리는 그러한 메커니즘을 추가하고 싶지 않습니다. 언젠가 이 문제가 해결된다면 대입 표현식 명세에서 이 특별한 경우를 제거할 수도 있습니다. 컴프리헨션에서 클래스 스코프에 정의된 변수를 사용하는 경우에도 이미 같은 문제가 있다는 점에 유의하십시오.)
컴프리헨션 안 대상에 관한 규칙이 동등한 코드로 어떻게 옮겨지는지 보여 주는 몇 가지 예는 부록 B를 참조하십시오.
:=의 상대적 우선순위
:= 연산자는 사용할 수 있는 모든 구문 위치에서 쉼표보다 더 강하게 결합하지만, or, and, not, 조건 표현식(A if C else B)을 비롯한 다른 모든 연산자보다는 약하게 결합합니다. 앞의 “예외적인 경우” 절에서 알 수 있듯이, 이 연산자는 =와 같은 수준에서는 절대 사용할 수 없습니다. 다른 방식으로 묶으려면 괄호를 사용해야 합니다.
:= 연산자는 함수 호출의 위치 인자에 바로 사용할 수 있지만, 키워드 인자에 바로 사용하는 것은 유효하지 않습니다.
기술적으로 무엇이 유효하고 무엇이 유효하지 않은지 명확히 보여 주는 몇 가지 예입니다.:
# INVALID
x := 0
# Valid alternative
(x := 0)
# INVALID
x = y := 0
# Valid alternative
x = (y := 0)
# Valid
len(lines := f.readlines())
# Valid
foo(x := 3, cat='vector')
# INVALID
foo(cat=category := 'vector')
# Valid alternative
foo(cat=(category := 'vector'))
위의 “유효한” 예 대부분은 권장하지 않습니다. Python 소스 코드를 빠르게 훑어보는 사람은 그 차이를 놓칠 수 있기 때문입니다. 하지만 단순한 경우는 문제 될 것이 없습니다.:
# Valid
if any(len(longline := line) >= 100 for line in lines):
print("Extremely long line:", longline)
이 PEP는 할당에 사용되는 =에 대한 PEP 8의 권고와 마찬가지로 := 주위에 항상 공백을 둘 것을 권장합니다. 반면 후자는 키워드 인자에 사용되는 = 주위에는 공백을 허용하지 않습니다.)
평가 순서 변경
의미를 정확하게 정의하기 위해, 이 제안은 평가 순서가 명확하게 정의되어 있어야 한다고 요구합니다. 함수 호출은 이미 부작용을 일으킬 수 있으므로, 이는 엄밀히 말해 새로운 요구 사항이 아닙니다. Python에는 이미 하위 표현식이 일반적으로 왼쪽에서 오른쪽으로 평가된다는 규칙이 있습니다. 그러나 대입 표현식은 이러한 부작용을 더 잘 드러나게 하므로, 현재 평가 순서에 대해 다음 한 가지 변경을 제안합니다:
- 딕셔너리 컴프리헨션
{X: Y for ...}에서는 현재Y가X보다 먼저 평가됩니다. 이를X가Y보다 먼저 평가되도록 변경할 것을 제안합니다. ({X: Y}같은 딕셔너리 디스플레이에서는 이미 그렇게 동작하며, 딕셔너리 컴프리헨션과 명백히 동등해야 하는dict((X, Y) for ...)에서도 마찬가지입니다.)
대입 표현식과 대입문의 차이점
가장 중요한 점은, :=는 표현식이므로 람다 함수와 컴프리헨션을 포함하여 문장을 쓸 수 없는 문맥에서도 사용할 수 있다는 것입니다.
반대로, 대입 표현식은 대입문에 있는 다음과 같은 고급 기능을 지원하지 않습니다:
- 다중 대상은 직접 지원되지 않습니다:
x = y = z = 0 # Equivalent: (z := (y := (x := 0)))
- 단일
NAME이외의 단일 대입 대상은 지원되지 않습니다:# No equivalent a[i] = x self.rest = []
- 쉼표 주변의 우선순위가 다릅니다:
x = 1, 2 # Sets x to (1, 2) (x := 1, 2) # Sets x to 1
- 이터러블 패킹 및 언패킹(일반 형식과 확장 형식 모두)은 지원되지 않습니다:
# Equivalent needs extra parentheses loc = x, y # Use (loc := (x, y)) info = name, phone, *rest # Use (info := (name, phone, *rest)) # No equivalent px, py, pz = position name, phone, email, *other_info = contact
- 인라인 타입 어노테이션은 지원되지 않습니다:
# Closest equivalent is "p: Optional[int]" as a separate declaration p: Optional[int] = None
- 복합 대입은 지원되지 않습니다:
total += tax # Equivalent: (total := total + tax)
구현 중 명세 변경 사항
PEP가 처음 승인된 후 Python 3.8이 릴리스되기 전까지, 구현 경험과 추가 검토를 바탕으로 다음과 같은 변경이 이루어졌습니다:
- 다른 유사한 예외와의 일관성을 위해, 그리고 최종 사용자에게 반드시 더 명확하다고 할 수 없는 예외 이름에 묶이는 것을 피하기 위해, 원래 제안되었던
SyntaxError의 서브클래스TargetScopeError는 폐기되었으며, 대신SyntaxError를 직접 발생시키도록 바뀌었습니다. [3] - CPython의 심볼 테이블 분석 과정의 한계로 인해, 참조 구현은 이름 있는 표현식의 대상이 컴프리헨션의 반복 변수 중 하나와 충돌할 때만
SyntaxError를 발생시키는 것이 아니라, 컴프리헨션의 이터러블 표현식 안에서 이름 있는 표현식을 사용하는 모든 경우에SyntaxError를 발생시킵니다. 충분히 설득력 있는 예가 제시된다면 이 문제를 다시 검토할 수 있지만, 더 선택적인 제한을 구현하는 데 필요한 추가 복잡성은 순전히 가상의 사용 사례를 위해 감수할 가치가 없어 보입니다.
예시
Python 표준 라이브러리의 예
site.py
env_base는 이 줄들에서만 사용되므로, 그 대입을 if 문에 두면 대입이 블록의 “헤더”로 옮겨집니다.
- 현재:
env_base = os.environ.get("PYTHONUSERBASE", None) if env_base: return env_base
- 개선:
if env_base := os.environ.get("PYTHONUSERBASE", None): return env_base
_pydecimal.py
중첩된 if 문을 피하고 들여쓰기 한 단계를 제거합니다.
- 현재:
if self._is_special: ans = self._check_nans(context=context) if ans: return ans
- 개선:
if self._is_special and (ans := self._check_nans(context=context)): return ans
copy.py
코드가 더 규칙적으로 보이며 여러 겹으로 중첩된 if를 피할 수 있습니다. (이 예제의 출처는 부록 A를 참조하십시오.)
- 현재:
reductor = dispatch_table.get(cls) if reductor: rv = reductor(x) else: reductor = getattr(x, "__reduce_ex__", None) if reductor: rv = reductor(4) else: reductor = getattr(x, "__reduce__", None) if reductor: rv = reductor() else: raise Error( "un(deep)copyable object of type %s" % cls)
- 개선:
if reductor := dispatch_table.get(cls): rv = reductor(x) elif reductor := getattr(x, "__reduce_ex__", None): rv = reductor(4) elif reductor := getattr(x, "__reduce__", None): rv = reductor() else: raise Error("un(deep)copyable object of type %s" % cls)
datetime.py
tz는 s += tz에만 사용되므로, 그 대입을 if 안으로 옮기면 해당 변수의 스코프를 드러내는 데 도움이 됩니다.
- 현재:
s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) tz = self._tzstr() if tz: s += tz return s
- 개선:
s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) if tz := self._tzstr(): s += tz return s
sysconfig.py
while 조건에서 fp.readline()을 호출하고 if 줄에서 .match()를 호출하면 코드를 이해하기 어렵게 만들지 않으면서도 더 간결하게 만들 수 있습니다.
- 현재:
while True: line = fp.readline() if not line: break m = define_rx.match(line) if m: n, v = m.group(1, 2) try: v = int(v) except ValueError: pass vars[n] = v else: m = undef_rx.match(line) if m: vars[m.group(1)] = 0
- 개선:
while line := fp.readline(): if m := define_rx.match(line): n, v = m.group(1, 2) try: v = int(v) except ValueError: pass vars[n] = v elif m := undef_rx.match(line): vars[m.group(1)] = 0
리스트 컴프리헨션 단순화하기
리스트 컴프리헨션은 조건을 캡처함으로써 매핑과 필터링을 효율적으로 수행할 수 있습니다:
results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0]
마찬가지로, 하위 표현식을 처음 사용할 때 이름을 붙여 주면 주 표현식 안에서 재사용할 수 있습니다:
stuff = [[y := f(x), x/y] for x in range(5)]
두 경우 모두 변수 y는 이를 감싸는 스코프(즉, results나 stuff와 같은 수준)에 바인딩된다는 점에 유의하십시오.
조건 값 캡처하기
대입 표현식은 if나 while 문의 헤더에서 효과적으로 사용할 수 있습니다:
# Loop-and-a-half
while (command := input("> ")) != "quit":
print("You entered:", command)
# Capturing regular expression match objects
# See, for instance, Lib/pydoc.py, which uses a multiline spelling
# of this effect
if match := re.search(pat, text):
print("Found:", match.group(0))
# The same syntax chains nicely into 'elif' statements, unlike the
# equivalent using assignment statements.
elif match := re.search(otherpat, text):
print("Alternate found:", match.group(0))
elif match := re.search(third, text):
print("Fallback found:", match.group(0))
# Reading socket data until an empty string is returned
while data := sock.recv(8192):
print("Received data:", data)
특히 while 루프에서는 무한 루프, 대입, 조건을 따로 둘 필요를 없앨 수 있습니다. 또한 단순히 함수 호출을 조건으로 사용하는 루프와, 그것을 조건으로 사용하면서 실제 값도 함께 사용하는 루프 사이에 자연스러운 대응 관계를 만들어 줍니다.
Fork
저수준 UNIX 세계의 예제:
if pid := os.fork():
# Parent code
else:
# Child code
기각된 대안 제안
이 제안과 대체로 비슷한 제안들이 python-ideas에 자주 올라왔습니다. 아래는 위에서 제시한 문법을 채택하면서 기각된 여러 대안 문법이며, 그중 일부는 컴프리헨션에만 해당합니다.
컴프리헨션의 스코프 규칙 변경
이 PEP의 이전 버전에서는 컴프리헨션을 클래스 스코프에서 더 쓸모 있게 만들고 “가장 바깥쪽 이터러블”과 컴프리헨션 나머지 부분의 스코프를 통일하기 위해 컴프리헨션의 스코프 규칙을 미묘하게 변경할 것을 제안했습니다. 그러나 제안의 이 부분은 하위 호환성을 깨뜨렸을 것이므로, PEP가 대입 표현식에 집중할 수 있도록 철회되었습니다.
대안 표기법
현재 제안과 의미는 대체로 같지만 표기가 다른 방식입니다.
EXPR as NAME:stuff = [[f(x) as y, x/y] for x in range(5)]
EXPR as NAME은 이미import,except,with문에서 (다른 의미로) 쓰이고 있으므로, 이는 불필요한 혼란을 일으키거나 특수한 처리(예: 이러한 문의 헤더 안에서 대입을 금지하는 것)를 요구하게 됩니다.(
with EXPR as VAR는 단순히EXPR의 값을VAR에 대입하는 것이 아니라는 점에 유의하십시오 – 이 문은EXPR.__enter__()를 호출하고 그 결과를VAR에 대입합니다.)이 표기보다
:=를 선호하는 추가적인 이유는 다음과 같습니다:if f(x) as y에서는 대입 대상이 눈에 잘 띄지 않습니다 – 그저if f x blah blah처럼 읽힐 뿐이며, 시각적으로if f(x) and y와 너무 비슷합니다.as절이 허용되는 다른 모든 상황에서는 중급 수준의 독자라도 줄을 시작하는 키워드를 보고 (선택 사항이기는 하지만) 그 절이 나올 것을 예상하게 되며, 문법 또한 그 키워드를 as 절과 긴밀하게 묶어 줍니다:import foo as barexcept Exc as varwith ctxmgr() as var
이와 반대로, 대입 표현식은 줄을 시작하는
if나while에 속하지 않으며, 우리는 다른 문맥에서도 대입 표현식을 의도적으로 허용합니다.- 다음 두 형태 사이의 평행한 운율은
NAME = EXPRif NAME := EXPR
대입 표현식을 시각적으로 알아보기 쉽게 해 줍니다.
EXPR -> NAME:stuff = [[f(x) -> y, x/y] for x in range(5)]
이 문법은 R과 Haskell 같은 언어와 일부 프로그래밍 가능한 계산기에서 영감을 받았습니다. (왼쪽을 향하는 화살표
y <- f(x)는 Python에서 사용할 수 없다는 점에 유의하십시오. 미만 비교 연산자와 단항 마이너스로 해석되기 때문입니다.) 이 문법은with,except,import와 충돌하지 않는다는 점에서 ‘as’보다 약간 유리하지만, 그 밖에는 동등합니다. 그러나 이 문법은 Python에서->를 사용하는 다른 용도(함수 반환 타입 어노테이션)와 전혀 관련이 없으며, (Algol-58까지 거슬러 올라가는):=에 비하면 전통이 훨씬 약합니다.- 문장 지역 이름 앞에 점을 붙여 꾸미기:
stuff = [[(f(x) as .y), x/.y] for x in range(5)] # with "as" stuff = [[(.y := f(x)), x/.y] for x in range(5)] # with ":="
이 방식은 누출된 사용을 쉽게 감지할 수 있어 일부 형태의 문법적 모호성을 없앤다는 장점이 있습니다. 그러나 이렇게 하면 Python에서 변수의 스코프가 이름에 담기는 유일한 곳이 되어 리팩터링이 더 어려워집니다.
- 지역 이름 바인딩을 만들기 위해 모든 문에
where:를 추가하기:value = x**2 + 2*x where: x = spam(1, 4, 7, q)
실행 순서가 뒤바뀝니다(들여쓰기된 본문이 먼저 실행되고, 그다음에 “헤더”가 실행됩니다). 기존 키워드를 다른 용도로 쓰지 않는 한 새로운 키워드가 필요합니다(기존 키워드를 쓴다면
with:일 가능성이 가장 높습니다). 이 주제에 대한 이전 논의는 PEP 3150을 참조하십시오(여기서 제안된 키워드는given:입니다). TARGET from EXPR:stuff = [[y from f(x), x/y] for x in range(5)]
이 문법은
as보다 충돌이 적지만(raise Exc from Exc표기법과만 충돌합니다), 그 외에는as와 비슷합니다. 이 문법은with expr as target:과 평행을 이루는 대신(이러한 평행은 유용할 수도 있지만 혼란스러울 수도 있습니다) 평행을 이루는 대상이 없지만, 의미를 잘 떠올리게 합니다.
조건문을 특수하게 처리하기
가장 인기 있는 사용 사례 중 하나는 if와 while 문입니다. 이 제안은 더 일반적인 해법 대신, 이 두 문의 문법을 확장하여 비교된 값을 포착하는 수단을 추가합니다:
if re.search(pat, text) as match:
print("Found:", match.group(0))
이 방식은 원하는 조건이 포착된 값의 참 여부에 기반할 때, 그리고 오직 그때에만 훌륭하게 동작합니다. 따라서 특정 사용 사례(정규식 일치, 완료되면 ''를 반환하는 소켓 읽기)에는 효과적이지만, 더 복잡한 경우(예: 조건이 f(x) < 0이고 f(x)의 값을 포착하고 싶은 경우)에는 전혀 쓸모가 없습니다. 또한 리스트 컴프리헨션에는 아무런 이점이 없습니다.
장점: 문법적 모호성이 없습니다. 단점: if/while 문에서조차 가능한 사용 사례 중 일부에만 대응합니다.
컴프리헨션 특수 처리
또 다른 흔한 사용 사례는 컴프리헨션(리스트/집합/딕셔너리 및 제너레이터 표현식)입니다. 앞에서와 마찬가지로, 컴프리헨션에 특화된 해결책들이 제안되었습니다.
where,let또는given:stuff = [(y, x/y) where y = f(x) for x in range(5)] stuff = [(y, x/y) let y = f(x) for x in range(5)] stuff = [(y, x/y) given y = f(x) for x in range(5)]
이 방식은 부분 표현식을 ‘for’ 루프와 표현식 사이의 위치로 옮깁니다. 이 방식은 추가 언어 키워드를 도입하며, 이는 충돌을 일으킵니다. 세 가지 중
where가 가장 깔끔하게 읽히지만, 충돌 가능성도 가장 큽니다(예를 들어 SQLAlchemy와 numpy에는where메서드가 있으며, 표준 라이브러리의tkinter.dnd.Icon에도 있습니다).with NAME = EXPR:stuff = [(y, x/y) with y = f(x) for x in range(5)]
위와 같지만,
with키워드를 재사용합니다. 읽기에 그리 나쁘지 않고, 추가 언어 키워드도 필요하지 않습니다. 하지만 컴프리헨션에만 한정되며, “풀어 쓴” for 루프 문법으로 그만큼 쉽게 변환할 수 없습니다. 표현식 안의 등호가 비교를 수행하는 대신 이름 바인딩을 만들 수 있게 된다는 C의 문제를 안고 있습니다. “with NAME = EXPR:”를 왜 그 자체로 독립된 문장으로 사용할 수 없는지에 대한 의문을 불러일으킬 것입니다.with EXPR as NAME:stuff = [(y, x/y) with f(x) as y for x in range(5)]
선택지 2와 같지만, 등호 대신
as를 사용합니다. 이름 바인딩을 위한as의 다른 용법과 문법적으로 일관되지만, for 루프로 단순히 풀어 쓰면 의미가 크게 달라집니다. 컴프리헨션 안에서with의 의미는 독립적인 문장에서의 의미와 완전히 다르면서도, 문법은 동일하게 유지됩니다.
어떤 표기를 선택하든, 이는 컴프리헨션과 그에 해당하는 풀어 쓴 형태의 루프 사이에 뚜렷한 차이를 만듭니다. 이름 바인딩을 고치지 않고서는 더 이상 루프를 문장 형태로 풀어낼 수 없습니다. 이 용도로 재활용할 수 있는 유일한 키워드는 with뿐이며, 그 결과 컴프리헨션 안에서는 문장에서와 교묘하게 다른 의미를 갖게 됩니다. 아니면 새로운 키워드가 필요한데, 여기에는 그에 따르는 모든 비용이 뒤따릅니다.
연산자 우선순위 낮추기
:= 연산자에 대해 논리적으로 가능한 우선순위는 두 가지입니다. 문장 대입처럼 가능한 한 느슨하게 결합하거나, 비교 연산자보다 더 강하게 결합해야 합니다. 우선순위를 비교 연산자와 산술 연산자 사이(정확히는 비트 OR 바로 아래)에 두면 while과 if 조건 안에서의 대부분의 사용을 괄호 없이 작성할 수 있습니다. 어떤 값을 포착한 다음 그 값에 대해 비교를 수행하려는 경우가 가장 많기 때문입니다:
pos = -1
while pos := buffer.find(search_term, pos + 1) >= 0:
...
find()가 -1을 반환하면 루프가 종료됩니다. :=가 =처럼 느슨하게 결합한다면, 이 코드는 비교의 결과(일반적으로 True 또는 False)를 포착하게 되며, 이는 덜 유용합니다.
이 동작은 여러 상황에서 편리하겠지만, “:= 연산자는 대입문과 똑같이 동작한다”라고 설명하는 것보다 설명하기가 더 어렵습니다. 그래서 :=의 우선순위는 =의 우선순위와 최대한 가깝게 정해졌습니다(단, 쉼표보다 더 강하게 결합한다는 점은 예외입니다).
오른쪽에 쉼표 허용하기
일부 비평가들은 대입 표현식이 오른쪽에 괄호 없는 튜플을 허용하여 다음 두 코드가 동등해져야 한다고 주장했습니다:
(point := (x, y))
(point := x, y)
(현재 버전의 제안에서는 후자가 ((point := x), y)와 동등합니다.)
그러나 이 입장을 채택하면, 함수 호출에서 사용될 때도 대입 표현식이 쉼표보다 느슨하게 결합한다는 결론에 논리적으로 이르게 되며, 그러면 다음과 같은 혼란스러운 동등성이 생깁니다:
foo(x := 1, y)
foo(x := (1, y))
덜 혼란스러운 선택지는 :=가 쉼표보다 더 강하게 결합하도록 하는 것입니다.
항상 괄호 요구하기
대입 표현식을 항상 괄호로 감싸도록 요구하자는 제안이 있었습니다. 그렇게 하면 많은 모호함이 해소될 것이며, 실제로 원하는 하위 표현식을 추출하려면 괄호가 필요한 경우가 많을 것입니다. 그러나 다음과 같은 경우에는 추가 괄호가 불필요하게 느껴집니다:
# Top level in if
if match := pattern.match(line):
return match.group(1)
# Short call
len(lines := f.readlines())
자주 제기되는 반론
기존 대입을 그냥 표현식으로 바꾸면 되지 않습니까?
C와 그 파생 언어들은 = 연산자를 Python처럼 문(statement)이 아니라 표현식으로 정의합니다. 이 때문에 더 많은 문맥에서 대입을 사용할 수 있으며, 여기에는 비교가 더 흔하게 쓰이는 문맥도 포함됩니다. if (x == y)와 if (x = y)는 구문상 비슷해 보이지만, 그 의미는 완전히 다릅니다. 따라서 이 제안에서는 이 차이를 분명히 하기 위해 :=를 사용합니다.
대입 표현식이 있는데 대입문이 굳이 필요합니까?
두 형식은 유연성이 서로 다릅니다. := 연산자는 더 큰 표현식 안에서 사용할 수 있습니다. 반면 = 문은 += 등으로 복합 대입 형태로 확장할 수 있고, 연쇄할 수 있으며, 속성과 서브스크립트에 대입할 수 있습니다.
하위 지역 스코프를 사용해서 네임스페이스 오염을 막으면 되지 않습니까?
이 제안의 이전 개정판에서는 (단일 문으로 제한되는) 하위 지역 스코프를 도입하여 이름 누출과 네임스페이스 오염을 막았습니다. 이는 여러 상황에서 분명한 이점이 있지만, 다른 많은 상황에서는 복잡성을 높이며, 그 비용이 이점으로 정당화되지 않습니다. 언어의 단순성을 위해, 여기서 만들어지는 이름 바인딩은 다른 모든 이름 바인딩과 정확히 동일하며, 클래스나 모듈 스코프에서 사용하면 외부에서 볼 수 있는 이름이 만들어진다는 점도 마찬가지입니다. 이는 for 루프나 다른 구문과 다르지 않으며, 같은 방식으로 해결할 수 있습니다. 더 이상 필요하지 않게 되면 del로 이름을 삭제하거나, 이름 앞에 밑줄을 붙이면 됩니다.
(저자는 제안을 이 방향으로 이끌도록 제안해 준 Guido van Rossum과 Christoph Groth에게 감사를 표합니다. [2])
스타일 가이드 권고 사항
표현식 대입은 때때로 문 대입과 동등하게 사용할 수 있으므로, 어느 쪽을 선호해야 하는지에 대한 질문이 생길 것입니다. PEP 8과 같은 스타일 가이드를 위해 두 가지 권고 사항을 제안합니다.
- 대입문과 대입 표현식을 모두 사용할 수 있다면 대입문을 선호하십시오. 대입문은 의도를 명확하게 선언합니다.
- 대입 표현식을 사용했을 때 실행 순서가 모호해진다면, 대신 문을 사용하도록 구조를 바꾸십시오.
감사의 글
저자들은 이 제안에 상당한 기여를 해 준 Alyssa Coghlan과 Steven D’Aprano, 그리고 구현을 도와준 core-mentorship 메일링 리스트의 회원들에게 감사를 표합니다.
부록 A: Tim Peters의 소견
다음은 Tim Peters가 이 주제에 대해 쓴 짧은 글입니다.
저는 “번잡한” 코드 줄을 싫어하며, 개념적으로 관련 없는 로직을 한 줄에 넣는 것도 싫어합니다. 그래서 예를 들어, 다음과 같이 쓰는 대신:
i = j = count = nerrors = 0
저는 다음을 선호합니다:
i = j = 0
count = 0
nerrors = 0
대신 말입니다. 그래서 저는 대입 표현식을 쓰고 싶은 곳이 별로 없으리라고 짐작했습니다. 이미 화면 절반 가까이 뻗어 있는 줄에는 아예 고려조차 하지 않았습니다. 다른 경우에는 “관련 없음”이 우세했습니다:
mylast = mylast[1]
yield mylast[0]
다음 코드는 더 간결한 다음 코드보다 훨씬 낫습니다:
yield (mylast := mylast[1])[0]
원래의 두 문장은 개념적으로 전혀 다른 일을 하고 있으며, 이 둘을 한데 욱여넣는 것은 개념적으로 말이 되지 않습니다.
다른 경우에는 관련된 로직을 합치면 오히려 이해하기가 더 어려워졌습니다. 예를 들어 다음 코드를:
while True:
old = total
total += term
if old == total:
return total
term *= mx2 / (i*(i+1))
i += 2
더 간결한 다음 코드로 바꾸는 경우입니다:
while total != (total := total + term):
term *= mx2 / (i*(i+1))
i += 2
return total
여기서 while 검사는 너무 미묘합니다. 단락 평가도 메서드 체이닝도 아닌 문맥에서 엄격한 왼쪽에서 오른쪽 순서의 평가에 결정적으로 의존하기 때문입니다. 제 머리는 그런 식으로 돌아가지 않습니다.
하지만 그런 경우는 드물었습니다. 이름 바인딩은 매우 자주 일어나며, “성긴 것이 빽빽한 것보다 낫다”는 말이 “거의 비어 있는 것이 성긴 것보다 낫다”는 뜻은 아닙니다. 예를 들어, 저에게는 “이 경우에는 돌려줄 만한 유용한 것이 없지만, 이런 일이 자주 있을 것으로 예상되므로 예외로 귀찮게 하지는 않겠습니다”라는 뜻을 전하려고 None이나 0을 반환하는 함수가 많습니다. 이는 정규 표현식 검색 함수가 일치하는 것이 없을 때 None을 반환하는 것과 본질적으로 같습니다. 그래서 다음과 같은 형태의 코드가 많았습니다:
result = solution(xs, n)
if result:
# use result
저는 이것을 다음과 같이 쓰는 편이 더 명확하고, 타이핑도 확실히 조금 줄고 패턴을 맞춰 가며 읽을 부분도 줄어든다고 생각합니다:
if result := solution(xs, n):
# use result
약간의 가로 공백을 내주는 대신 주변 코드를 한 _줄_ 더 화면에 띄울 수 있다는 점도 좋습니다. 처음에는 이 점을 그다지 중요하게 여기지 않았지만, 워낙 자주 일어나다 보니 효과가 쌓였고, 머지않아 더 간결한 코드를 실제로 실행할 수 없다는 사실에 짜증이 나게 되었습니다. 저 스스로도 놀랐습니다!
대입 표현식이 정말로 빛을 발하는 경우는 또 있습니다. 제 코드에서 예를 하나 더 고르는 대신, Kirill Balunov가 표준 라이브러리 copy.py에 있는 copy() 함수에서 멋진 예를 가져왔습니다:
reductor = dispatch_table.get(cls)
if reductor:
rv = reductor(x)
else:
reductor = getattr(x, "__reduce_ex__", None)
if reductor:
rv = reductor(4)
else:
reductor = getattr(x, "__reduce__", None)
if reductor:
rv = reductor()
else:
raise Error("un(shallow)copyable object of type %s" % cls)
계속 깊어지는 들여쓰기는 의미상 오해를 불러일으킵니다. 이 로직은 개념적으로 평평하며, “처음으로 성공한 검사가 이긴다”는 것입니다:
if reductor := dispatch_table.get(cls):
rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
rv = reductor(4)
elif reductor := getattr(x, "__reduce__", None):
rv = reductor()
else:
raise Error("un(shallow)copyable object of type %s" % cls)
간편한 대입 표현식을 사용하면 코드의 시각적 구조가 로직의 개념적 평평함을 강조할 수 있습니다. 계속 깊어지는 들여쓰기는 이를 가려 버렸습니다.
제 코드에서 가져온 더 작은 예는 저를 기쁘게 했는데, 본질적으로 관련된 로직을 한 줄에 넣을 수 있게 해 주었고, 성가신 “인위적인” 들여쓰기 수준도 없앨 수 있게 해 주었습니다:
diff = x - x_base
if diff:
g = gcd(diff, n)
if g > 1:
return g
이 코드가 다음과 같이 바뀌었습니다:
if (diff := x - x_base) and (g := gcd(diff, n)) > 1:
return g
저 if는 제가 한 줄에 허용하고 싶은 길이의 거의 한계이지만, 여전히 따라 읽기 쉽습니다.
그러니 전체적으로 보면, 이름을 바인딩하는 대부분의 줄에서 저는 대입 표현식을 쓰지 않겠지만, 그 구문이 워낙 자주 쓰이기 때문에 대입 표현식을 쓸 곳도 많이 남습니다. 그런 곳 대부분에서는 작은 이득을 얻었는데, 워낙 자주 일어나기 때문에 그 이득이 쌓였으며, 나머지 곳에서는 중간에서 큰 정도의 이득을 얻었습니다. 저는 분명 삼항 if보다는 이것을 더 자주 사용하겠지만, 복합 대입보다는 훨씬 덜 사용할 것입니다.
수치 예제
당시 꽤 인상 깊었던 예가 하나 더 있습니다.
모든 변수가 양의 정수이고 a가 x의 n제곱근 이상일 때, 이 알고리즘은 x의 n제곱근의 바닥값(floor)을 반환합니다(그리고 반복할 때마다 정확한 비트 수가 대략 두 배로 늘어납니다):
while a > (d := x // a**(n-1)):
a = ((n-1)*a + d) // n
return a
그것이 왜 동작하는지는 분명하지 않지만, “루프 앤 하프(loop and a half)” 형태에서도 더 분명해지지는 않습니다. 올바른 통찰(“산술 평균 - 기하 평균 부등식”)에 기반하지 않고, 중첩된 floor 함수가 어떻게 동작하는지에 관한 몇 가지 자명하지 않은 사실을 알지 못하면 정확성을 증명하기 어렵습니다. 즉, 어려움은 코딩이 아니라 수학에 있습니다.
그 모든 것을 알고 있다면, 대입 표현식 형태는 “현재 추정값이 너무 큰 동안 더 작은 추정값 구하기”로 쉽게 읽히며, 여기서 “너무 큰가?” 검사와 새 추정값은 비용이 큰 하위 표현식을 공유합니다.
제가 보기에는 원래 형태가 더 이해하기 어렵습니다:
while True:
d = x // a**(n-1)
if a <= d:
break
a = ((n-1)*a + d) // n
return a
부록 B: 컴프리헨션의 대략적인 코드 변환
이 부록은 컴프리헨션이나 제너레이터 표현식에 대상이 나타날 때의 규칙을 (명세하지는 않지만) 명확히 하고자 합니다. 여러 예시를 통해 컴프리헨션을 포함하는 원래 코드와, 그 컴프리헨션을 동등한 제너레이터 함수와 약간의 보조 코드로 대체한 변환 결과를 보여 드립니다.
[x for ...]는 list(x for ...)와 동등하므로, 이 예시들은 일반성을 잃지 않고 모두 리스트 컴프리헨션을 사용합니다. 또한 이 예시들은 규칙의 경계 사례를 명확히 하기 위한 것이므로, 실제 코드처럼 보이도록 작성하지 않았습니다.
참고: 컴프리헨션은 이미 이 부록에 있는 것과 같은 중첩된 제너레이터 함수를 합성하는 방식으로 구현되어 있습니다. 새로운 부분은 대입 표현식 대상의 의도된 스코프(가장 바깥쪽 컴프리헨션을 포함하는 블록에서 대입을 수행한 것처럼 결정되는 스코프와 같은 스코프)를 확립하기 위해 적절한 선언을 추가하는 것입니다. 타입 추론 관점에서, 이 예시용 전개는 대입 표현식 대상이 항상 Optional이라는 뜻이 아닙니다(다만 대상이 바인딩되는 스코프는 나타냅니다).
먼저 대입 표현식이 없는 제너레이터 표현식에 대해 어떤 코드가 생성되는지 다시 살펴봅시다.
- 원래 코드(EXPR은 보통 VAR을 참조합니다):
def f(): a = [EXPR for VAR in ITERABLE]
- 변환 결과(이름 충돌은 신경 쓰지 맙시다):
def f(): def genexpr(iterator): for VAR in iterator: yield EXPR a = list(genexpr(iter(ITERABLE)))
간단한 대입 표현식을 추가해 봅시다.
- 원래 코드:
def f(): a = [TARGET := EXPR for VAR in ITERABLE]
- 변환 결과:
def f(): if False: TARGET = None # Dead code to ensure TARGET is a local variable def genexpr(iterator): nonlocal TARGET for VAR in iterator: TARGET = EXPR yield TARGET a = list(genexpr(iter(ITERABLE)))
f()에 global TARGET 선언을 추가해 봅시다.
- 원래 코드:
def f(): global TARGET a = [TARGET := EXPR for VAR in ITERABLE]
- 변환 결과:
def f(): global TARGET def genexpr(iterator): global TARGET for VAR in iterator: TARGET = EXPR yield TARGET a = list(genexpr(iter(ITERABLE)))
아니면 대신 f()에 nonlocal TARGET 선언을 추가해 봅시다.
- 원래 코드:
def g(): TARGET = ... def f(): nonlocal TARGET a = [TARGET := EXPR for VAR in ITERABLE]
- 변환 결과:
def g(): TARGET = ... def f(): nonlocal TARGET def genexpr(iterator): nonlocal TARGET for VAR in iterator: TARGET = EXPR yield TARGET a = list(genexpr(iter(ITERABLE)))
마지막으로, 두 컴프리헨션을 중첩해 봅시다.
- 원래 코드:
def f(): a = [[TARGET := i for i in range(3)] for j in range(2)] # I.e., a = [[0, 1, 2], [0, 1, 2]] print(TARGET) # prints 2
- 변환 결과:
def f(): if False: TARGET = None def outer_genexpr(outer_iterator): nonlocal TARGET def inner_generator(inner_iterator): nonlocal TARGET for i in inner_iterator: TARGET = i yield i for j in outer_iterator: yield list(inner_generator(range(3))) a = list(outer_genexpr(range(2))) print(TARGET)
부록 C: 스코프 의미론은 변경되지 않음
혼란을 일으켜 온 부분이므로 짚어 두자면, Python의 스코프 의미는 전혀 바뀌지 않습니다. 함수 지역 스코프는 계속해서 컴파일 시점에 결정되며, 실행 시점에는 무기한의 시간적 범위를 가집니다(“완전한 클로저”). 예시:
a = 42
def f():
# `a` is local to `f`, but remains unbound
# until the caller executes this genexp:
yield ((a := i) for i in range(3))
yield lambda: a + 100
print("done")
try:
print(f"`a` is bound to {a}")
assert False
except UnboundLocalError:
print("`a` is not yet bound")
그런 다음:
>>> results = list(f()) # [genexp, lambda]
done
`a` is not yet bound
# The execution frame for f no longer exists in CPython,
# but f's locals live so long as they can still be referenced.
>>> list(map(type, results))
[<class 'generator'>, <class 'function'>]
>>> list(results[0])
[0, 1, 2]
>>> results[1]()
102
>>> a
42
참고 문헌
Copyright
This document has been placed in the public domain.