[Final] PEP 572 - Assignment Expressions
원문 링크: PEP 572 - Assignment Expressions
상태: Final 유형: Standards Track 작성일: 28-Feb-2018
다음은 PEP 572 – 할당 표현식(Assignment Expressions) 문서의 번역 및 정리 내용입니다. 이 PEP는 Python 3.8에 도입된 :=
연산자를 설명하며, 이를 통해 표현식 내부에서 변수에 값을 할당할 수 있게 됩니다. 이 연산자는 흔히 “walrus operator(바다코뿔소 연산자)”로 불립니다.
PEP 572 – 할당 표현식 (Assignment Expressions)
- 작성자: Chris Angelico, Tim Peters, Guido van Rossum
- 상태: Final
- 유형: Standards Track
- 작성일: 2018년 2월 28일
- Python 버전: 3.8
- 해결: Python-Dev 메시지
개요 (Abstract)
이 PEP는 NAME := expr
표기법을 사용하여 표현식(expression) 내에서 변수에 값을 할당하는 방법을 제안합니다. 이 변경의 일환으로, 딕셔너리 컴프리헨션(dictionary comprehension)의 평가 순서도 업데이트되어 키 표현식이 값 표현식보다 먼저 실행되도록 합니다 (키가 이름에 바인딩된 다음 해당 값을 계산하는 데 재사용될 수 있도록 허용).
이 PEP 논의 중에 이 연산자는 비공식적으로 “walrus operator(바다코뿔소 연산자)”로 알려지게 되었습니다. 이 구성의 공식 명칭은 “Assignment Expressions(할당 표현식)”이지만, “Named Expressions(이름 붙은 표현식)”이라고도 불릴 수 있습니다 (예: CPython 참조 구현에서는 내부적으로 이 이름을 사용합니다).
배경 (Rationale)
표현식의 결과에 이름을 지정하는 것은 프로그래밍의 중요한 부분입니다. 이는 길어진 표현식 대신 설명적인 이름을 사용할 수 있게 하고, 재사용을 허용합니다. 현재 이 기능은 구문(statement) 형태로만 제공되므로 List Comprehension
및 기타 표현식 컨텍스트에서는 사용할 수 없었습니다.
또한, 큰 표현식의 하위 부분에 이름을 지정하면 대화형 디버거(debugger)를 지원하여 유용한 표시 후크(display hooks)와 부분 결과(partial results)를 제공할 수 있습니다. 하위 표현식을 인라인으로 캡처하는 방법이 없다면 원래 코드를 리팩토링(refactoring)해야 했을 것입니다. 할당 표현식을 사용하면 몇 개의 name :=
마커만 삽입하면 됩니다. 리팩토링의 필요성을 줄이면 디버깅 과정에서 코드가 의도치 않게 변경될 가능성(흔히 하이젠버그(Heisenbugs)의 원인)이 줄어들고, 다른 프로그래머에게 지시하기도 더 쉽습니다.
실제 코드의 중요성 (The importance of real code)
이 PEP의 개발 과정에서 많은 사람(지지자와 비판자 모두)이 한편으로는 장난감 예제(toy examples)에, 다른 한편으로는 지나치게 복잡한 예제에 집중하는 경향이 있었습니다.
장난감 예제의 위험은 두 가지입니다: 종종 너무 추상적이어서 아무도 “오, 정말 설득력 있네”라고 생각하게 만들지 못하며, “어쨌든 나는 그런 식으로 코드를 작성하지 않을 거야”라는 반박에 쉽게 반박당합니다.
지나치게 복잡한 예제의 위험은 제안 비판자들이 공격하기 편리한 허수아비(strawman)를 제공한다는 것입니다 (“저건 난독화된 코드야”).
그럼에도 불구하고 매우 간단한 예제와 매우 복잡한 예제 모두에 약간의 유용성이 있습니다: 이는 의도된 의미론(semantics)을 명확히 하는 데 도움이 됩니다. 따라서 아래에는 이러한 예제가 일부 포함될 것입니다.
그러나 설득력을 갖기 위해서는 예제가 실제 코드, 즉 이 PEP에 대해 전혀 생각하지 않고 유용한 애플리케이션의 일부로 작성된 코드(크든 작든)에 뿌리를 두어야 합니다. Tim Peters는 자신의 개인 코드 저장소를 검토하고 (그의 관점에서) 할당 표현식을 (드물게) 사용하여 다시 작성하면 더 명확해질 것이라고 생각하는 코드 예제를 고르는 데 매우 도움이 되었습니다. 그의 결론은 현재 제안이 상당히 많은 코드에서 적당하지만 명확한 개선을 가능하게 했을 것이라는 것입니다.
실제 코드의 또 다른 용도는 프로그래머가 코드의 간결성(compactness)에 얼마나 가치를 두는지 간접적으로 관찰하는 것입니다. Guido van Rossum은 Dropbox 코드 베이스를 검색하여 프로그래머가 더 짧은 줄(shorter lines)보다 더 적은 줄(fewer lines)을 작성하는 데 가치를 둔다는 증거를 발견했습니다.
대표적인 예시: 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
또 다른 예시는 프로그래머가 추가 들여쓰기(indentation) 수준을 절약하기 위해 때때로 더 많은 작업을 수행한다는 것을 보여줍니다:
match1 = pattern1.match(data)
match2 = pattern2.match(data) # pattern1이 일치해도 pattern2를 매치하려고 시도함
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
문법과 의미론 (Syntax and semantics)
임의의 Python 표현식을 사용할 수 있는 대부분의 컨텍스트에서 이름 붙은 표현식(named expression)이 나타날 수 있습니다. 이는 NAME := expr
형식이며, 여기서 expr
은 괄호 없는 튜플(unparenthesized tuple)을 제외한 모든 유효한 Python 표현식이며, NAME
은 식별자(identifier)입니다.
이러한 이름 붙은 표현식의 값은 포함된 표현식과 동일하며, 추가적으로 대상에 해당 값이 할당되는 부수 효과(side-effect)가 있습니다:
# 일치하는 정규식 처리
if (match := pattern.search(data)) is not None:
# match로 뭔가 수행
...
# 2-arg iter()를 사용하여 간단하게 재작성할 수 없는 루프
while chunk := file.read(8192):
process(chunk)
# 계산 비용이 비싼 값을 재사용
[y := f(x), y**2, y**3]
# 컴프리헨션 필터 절과 출력 사이에서 하위 표현식 공유
filtered_data = [y for x in data if (y := f(x)) is not None]
예외적인 경우 (Exceptional cases)
모호성이나 사용자 혼란을 피하기 위해 할당 표현식이 허용되지 않는 몇 가지 경우가 있습니다:
- 표현식 구문(expression statement)의 최상위 수준에서 괄호 없는 할당 표현식은 금지됩니다.
예시:
y := f(x) # INVALID (y := f(x)) # Valid, 권장하지 않음
이 규칙은 사용자에게 할당 구문과 할당 표현식 사이의 선택을 단순화하기 위해 포함되었습니다 – 둘 다 유효한 구문 위치는 없습니다.
- 할당 구문의 우측 최상위 수준에서 괄호 없는 할당 표현식은 금지됩니다.
예시:
y0 = y1 := f(x) # INVALID y0 = (y1 := f(x)) # Valid, 권장하지 않음
다시 말하지만, 이 규칙은 동일한 것을 표현하는 시각적으로 유사한 두 가지 방법을 피하기 위해 포함되었습니다.
- 함수 호출에서 키워드 인자(keyword argument)의 값으로 괄호 없는 할당 표현식은 금지됩니다.
예시:
foo(x = y := f(x)) # INVALID foo(x=(y := f(x))) # Valid, 하지만 혼란스러울 가능성이 높음
이 규칙은 지나치게 혼란스러운 코드를 허용하지 않기 위해, 그리고 키워드 인자 파싱(parsing)이 이미 충분히 복잡하기 때문에 포함되었습니다.
- 함수 기본값(default value)의 최상위 수준에서 괄호 없는 할당 표현식은 금지됩니다.
예시:
def foo(answer = p := 42): # INVALID ... def foo(answer=(p := 42)): # Valid, 하지만 좋은 스타일은 아님 ...
이 규칙은 많은 사용자에게 정확한 의미론이 이미 혼란스러운 위치(가변 기본값(mutable default values)에 대한 일반적인 스타일 권장 사항 참조)에서 부수 효과(side effects)를 권장하지 않기 위해, 그리고 호출에서의 유사한 금지(이전 항목)를 반영하기 위해 포함되었습니다.
- 인자, 반환 값 및 할당에 대한 어노테이션(annotations)으로 괄호 없는 할당 표현식은 금지됩니다.
예시:
def foo(answer: p := 42 = 5): # INVALID ... def foo(answer: (p := 42) = 5): # Valid, 하지만 아마 쓸모 없을 것 ...
여기서의 이유는 이전 두 경우와 유사합니다.
:
와=
로 구성된 이러한 그룹화되지 않은 기호 및 연산자 모음은 올바르게 읽기 어렵습니다. lambda
함수 내에서 괄호 없는 할당 표현식은 금지됩니다. 예시:(lambda: x := 1) # INVALID lambda: (x := 1) # Valid, 하지만 쓸모 없을 가능성이 높음 (x := lambda: 1) # Valid lambda line: (m := re.match(pattern, line)) and m.group(1) # Valid
이는
lambda
가 항상:=
보다 덜 강하게 바인딩되도록 허용합니다.lambda
함수 내의 최상위 수준에서 이름 바인딩을 갖는 것은 쓸모가 없을 가능성이 높습니다. 이름이 한 번 이상 사용될 경우 표현식은 어쨌든 괄호로 묶어야 할 가능성이 높으므로 이 금지는 코드에 거의 영향을 미치지 않을 것입니다.- f-string 내의 할당 표현식은 괄호를 필요로 합니다.
예시:
>>> f'{(x:=10)}' # Valid, 할당 표현식 사용 '10' >>> x = 10 >>> f'{x:=10}' # Valid, '=10'을 포매터에 전달 ' 10'
이는 f-string에서 할당 연산자처럼 보이는 것이 항상 할당 연산자가 아님을 보여줍니다. f-string 파서(parser)는
:
를 포맷팅 옵션을 나타내는 데 사용합니다. 하위 호환성(backwards compatibility)을 유지하기 위해 f-string 내부에서 할당 연산자 사용은 괄호로 묶어야 합니다. 위에서 언급했듯이, 할당 연산자의 이러한 사용은 권장되지 않습니다.
대상의 스코프 (Scope of the target)
할당 표현식은 새로운 스코프(scope)를 도입하지 않습니다. 대부분의 경우 대상이 바인딩될 스코프는 자명합니다: 현재 스코프입니다. 이 스코프에 대상에 대한 nonlocal
또는 global
선언이 포함되어 있다면, 할당 표현식은 이를 따릅니다. lambda
는 (익명이지만 명시적인) 함수 정의이므로 이 목적을 위한 스코프로 간주됩니다.
한 가지 특별한 경우가 있습니다: list
, set
, dict
컴프리헨션 또는 제너레이터 표현식(generator expression) (아래에서는 총칭하여 “컴프리헨션”이라고 함)에 나타나는 할당 표현식은 포함하는 스코프(containing scope)에 대상을 바인딩하며, 해당 스코프에 대상에 대한 nonlocal
또는 global
선언이 있다면 이를 따릅니다. 이 규칙의 목적상, 중첩된 컴프리헨션의 포함하는 스코프는 가장 바깥쪽 컴프리헨션을 포함하는 스코프입니다. lambda
는 포함하는 스코프로 간주됩니다.
이 특별한 경우의 동기는 두 가지입니다. 첫째, any()
표현식의 “증인” 또는 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)
둘째, 컴프리헨션에서 가변 상태(mutable state)를 업데이트하는 간결한 방법을 제공합니다. 예를 들어:
# 리스트 컴프리헨션에서 부분 합계 계산
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
이러한 경우에 일관된 의미론을 할당하는 것이 기술적으로 가능하더라도, 실제 사용 사례가 없는 상태에서 그 의미론이 실제로 합리적인지 판단하기는 어렵습니다. 따라서 참조 구현은 이러한 경우가 구현 정의 동작(implementation defined behaviour)으로 실행되는 대신 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
앞 부분)의 경우, 이 제한은 컴프리헨션의 이터레이션 변수(iteration variables)로도 사용되는 대상 이름에만 적용됩니다. 이러한 위치에 나타나는 lambda
표현식은 새로운 명시적 함수 스코프를 도입하며, 따라서 추가 제한 없이 할당 표현식을 사용할 수 있습니다.
참조 구현의 설계 제약(심볼 테이블 분석기(symbol table analyser)가 가장 왼쪽 컴프리헨션 이터러블 표현식과 컴프리헨션의 나머지 부분 사이에서 이름이 재사용되는 시점을 쉽게 감지할 수 없음)으로 인해, 이름 붙은 표현식은 컴프리헨션 이터러블 표현식(각 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
클래스 스코프(class scope)를 포함하는 컴프리헨션에서 할당 표현식이 발생할 때 추가 예외가 적용됩니다. 위의 규칙으로 인해 대상이 해당 클래스의 스코프에 할당될 경우, 할당 표현식은 명시적으로 유효하지 않습니다. 이 경우에도 SyntaxError
가 발생합니다:
class Example:
[(j := i) for i in range(5)] # INVALID
(후자의 예외에 대한 이유는 컴프리헨션을 위해 생성된 암시적 함수 스코프입니다 – 현재 함수가 포함하는 클래스 스코프의 변수를 참조할 수 있는 런타임 메커니즘이 없으며, 우리는 그러한 메커니즘을 추가하고 싶지 않습니다. 이 문제가 해결되면 이 특별한 경우는 할당 표현식의 사양에서 제거될 수 있습니다. 이 문제는 컴프리헨션에서 클래스 스코프에 정의된 변수를 사용하는 경우에도 이미 존재한다는 점에 유의하십시오).
컴프리헨션의 대상에 대한 규칙이 동등한 코드로 어떻게 번역되는지에 대한 몇 가지 예시는 부록 B를 참조하십시오.
:=
의 상대적 우선순위 (Relative precedence of :=
)
:=
연산자는 허용되는 모든 구문 위치에서 쉼표(comma)보다 더 강하게 그룹화되지만, or
, and
, not
및 조건 표현식(A if C else B
)을 포함한 다른 모든 연산자보다 덜 강하게 그룹화됩니다. 위의 “예외적인 경우” 섹션에서 알 수 있듯이, =
와 동일한 수준에서는 절대 허용되지 않습니다. 다른 그룹화가 필요할 경우 괄호를 사용해야 합니다.
:=
연산자는 위치 인자(positional function call argument)에 직접 사용할 수 있지만, 키워드 인자에는 직접 사용할 수 없습니다.
기술적으로 유효하거나 유효하지 않은 것을 명확히 하는 몇 가지 예제:
# INVALID
x := 0
# Valid 대체
(x := 0)
# INVALID
x = y := 0
# Valid 대체
x = (y := 0)
# Valid
len(lines := f.readlines())
# Valid
foo(x := 3, cat='vector')
# INVALID
foo(cat=category := 'vector')
# Valid 대체
foo(cat=(category := 'vector'))
위의 “유효한” 예제 대부분은 권장되지 않습니다. Python 소스 코드를 빠르게 훑어보는 독자가 그 차이를 놓칠 수 있기 때문입니다. 그러나 간단한 경우는 반대할 만하지 않습니다:
# Valid
if any(len(longline := line) >= 100 for line in lines):
print("Extremely long line:", longline)
이 PEP는 할당에 사용되는 =
에 대한 PEP 8의 권장 사항과 유사하게 :=
주변에 항상 공백을 두도록 권장합니다 (후자는 키워드 인자에 사용되는 =
주변에 공백을 허용하지 않습니다).
평가 순서 변경 (Change to evaluation order)
정확하게 정의된 의미론을 갖기 위해, 이 제안은 평가 순서가 잘 정의되어야 한다고 요구합니다. 이는 함수 호출이 이미 부수 효과를 가질 수 있으므로 기술적으로 새로운 요구 사항은 아닙니다. Python은 이미 하위 표현식이 일반적으로 왼쪽에서 오른쪽으로 평가된다는 규칙을 가지고 있습니다. 그러나 할당 표현식은 이러한 부수 효과를 더 잘 보이게 하며, 현재 평가 순서에 한 가지 변경 사항을 제안합니다:
dict
컴프리헨션 {X: Y for ...}
에서 Y
는 현재 X
보다 먼저 평가됩니다. 우리는 이를 X
가 Y
보다 먼저 평가되도록 변경할 것을 제안합니다. (dict
디스플레이 {X: Y}
에서는 이미 이러한 경우이며, dict((X, Y) for ...)
에서도 마찬가지인데, 이는 dict
컴프리헨션과 분명히 동등해야 합니다).
할당 표현식과 할당 구문의 차이점 (Differences between assignment expressions and assignment statements)
가장 중요하게는 :=
가 표현식이므로 lambda
함수 및 컴프리헨션을 포함하여 구문이 허용되지 않는 컨텍스트에서 사용할 수 있습니다.
반대로, 할당 표현식은 할당 구문에서 찾을 수 있는 고급 기능을 지원하지 않습니다:
- 다중 대상(Multiple targets)은 직접 지원되지 않습니다:
x = y = z = 0 # 동등: (z := (y := (x := 0)))
- 단일
NAME
이외의 단일 할당 대상은 지원되지 않습니다:# 동등한 것이 없음 a[i] = x self.rest = []
- 쉼표 주변의 우선순위가 다릅니다:
x = 1, 2 # x를 (1, 2)로 설정 (x := 1, 2) # x를 1로 설정
- 이터러블 패킹(packing) 및 언패킹(unpacking) (일반 또는 확장 형식 모두)은 지원되지 않습니다:
# 동등하려면 추가 괄호가 필요 loc = x, y # (loc := (x, y)) 사용 info = name, phone, *rest # (info := (name, phone, *rest)) 사용 # 동등한 것이 없음 px, py, pz = position name, phone, email, *other_info = contact
- 인라인 타입 어노테이션(Inline type annotations)은 지원되지 않습니다:
# 가장 유사한 동등은 별도의 선언으로 "p: Optional[int]" p: Optional[int] = None
- 증가 할당(Augmented assignment)은 지원되지 않습니다:
total += tax # 동등: (total := total + tax)
구현 중 사양 변경 (Specification changes during implementation)
PEP가 처음 승인된 후 Python 3.8이 출시되기 전에 구현 경험 및 추가 검토를 기반으로 다음과 같은 변경이 이루어졌습니다:
- 다른 유사한 예외와의 일관성을 위해, 그리고 최종 사용자에게 명확성을 반드시 향상시키지 않을 예외 이름을 고정하는 것을 피하기 위해, 원래 제안된
SyntaxError
의 하위 클래스인TargetScopeError
는SyntaxError
를 직접 발생시키는 것으로 대체되었습니다. - CPython의 심볼 테이블 분석 프로세스(symbol table analysis process)의 한계로 인해, 참조 구현은 이름 붙은 표현식의 대상이 컴프리헨션의 이터레이션 변수 중 하나와 충돌할 때만
SyntaxError
를 발생시키는 대신, 컴프리헨션 이터러블 표현식 내부의 이름 붙은 표현식의 모든 사용에 대해SyntaxError
를 발생시킵니다. 충분히 설득력 있는 예제가 있다면 재검토될 수 있지만, 더 선택적인 제한을 구현하는 데 필요한 추가적인 복잡성은 순전히 가상의 사용 사례에 대해 가치가 없어 보입니다.
예제 (Examples)
Python 표준 라이브러리의 예제 (Examples from the Python standard library)
site.py
env_base
는 이 줄에서만 사용되므로, if
에 할당을 넣으면 블록의 “헤더”로 이동합니다.
현재 (Current):
env_base = os.environ.get("PYTHONUSERBASE", None)
if env_base:
return env_base
개선 (Improved):
if env_base := os.environ.get("PYTHONUSERBASE", None):
return env_base
_pydecimal.py
중첩된 if
를 피하고 들여쓰기 수준을 하나 제거합니다.
현재 (Current):
if self._is_special:
ans = self._check_nans(context=context)
if ans:
return ans
개선 (Improved):
if self._is_special and (ans := self._check_nans(context=context)):
return ans
copy.py
코드가 더 규칙적으로 보이고 여러 중첩된 if
를 피합니다. (이 예제의 출처는 부록 A를 참조하십시오).
현재 (Current):
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)
**개선 (Improved):
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
내부로 이동하면 스코프를 보여주는 데 도움이 됩니다.
현재 (Current):
s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec)
tz = self._tzstr()
if tz:
s += tz
return s
개선 (Improved):
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()
를 호출하면 코드를 이해하기 어렵게 만들지 않으면서 더 간결하게 만듭니다.
현재 (Current):
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
개선 (Improved):
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
리스트 컴프리헨션 단순화 (Simplifying list comprehensions)
리스트 컴프리헨션은 조건을 캡처하여 효율적으로 매핑하고 필터링할 수 있습니다:
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
와 동일한 수준)에 바인딩됩니다.
조건 값 캡처 (Capturing condition values)
할당 표현식은 if
또는 while
구문의 헤더에서 효과적으로 사용될 수 있습니다:
# 절반 루프 (Loop-and-a-half)
while (command := input("> ")) != "quit":
print("You entered:", command)
# 정규식 매치 객체 캡처
# Lib/pydoc.py를 참조하십시오. 이 효과를 여러 줄로 사용합니다.
if match := re.search(pat, text):
print("Found:", match.group(0))
# 동일한 구문은 할당 구문을 사용하는 것과는 달리 'elif' 구문으로 깔끔하게 연결됩니다.
elif match := re.search(otherpat, text):
print("Alternate found:", match.group(0))
elif match := re.search(third, text):
print("Fallback found:", match.group(0))
# 빈 문자열이 반환될 때까지 소켓 데이터 읽기
while data := sock.recv(8192):
print("Received data:", data)
특히 while
루프의 경우, 이는 무한 루프, 할당 및 조건의 필요성을 제거할 수 있습니다. 또한 단순히 함수 호출을 조건으로 사용하는 루프와 이를 조건으로 사용하면서 실제 값을 활용하는 루프 사이에 부드러운 병렬 관계를 만듭니다.
Fork
낮은 수준의 UNIX 세계에서 온 예시:
if pid := os.fork():
# Parent code (부모 코드)
else:
# Child code (자식 코드)
거부된 대안 제안 (Rejected alternative proposals)
이와 유사한 제안들이 python-ideas
에서 자주 논의되었습니다. 아래는 현재 제안된 방식 대신 거부된 여러 대안 문법이며, 일부는 컴프리헨션에만 국한된 것이었습니다.
컴프리헨션의 스코프 규칙 변경 (Changing the scope rules for comprehensions)
이 PEP의 이전 버전은 컴프리헨션의 스코프 규칙에 미묘한 변경을 제안하여 클래스 스코프에서 더 유용하게 만들고 “가장 바깥쪽 이터러블”과 컴프리헨션의 나머지 부분의 스코프를 통일하고자 했습니다. 그러나 이 제안의 이 부분은 하위 호환성을 깨뜨릴 수 있었고, PEP가 할당 표현식에 집중할 수 있도록 철회되었습니다.
다른 표기법 (Alternative spellings)
현재 제안과 대체로 동일한 의미론을 갖지만, 다르게 표기된 방식들입니다.
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 bar except Exc as var with ctxmgr() as var
- 대조적으로, 할당 표현식은 줄을 시작하는
if
또는while
에 속하지 않으며, 다른 컨텍스트에서도 할당 표현식을 의도적으로 허용합니다. NAME = EXPR
과if NAME := EXPR
사이의 병렬 리듬은 할당 표현식의 시각적 인식을 강화합니다.
EXPR -> NAME
:stuff = [[f(x) -> y, x/y] for x in range(5)]
이 문법은 R 및 Haskell과 같은 언어와 일부 프로그래밍 가능한 계산기에서 영감을 받았습니다. (왼쪽 화살표
y <- f(x)
는 Python에서는 불가능합니다. 이는less-than
및 단항 마이너스(unary minus)로 해석될 것이기 때문입니다). 이 문법은with
,except
,import
와 충돌하지 않는다는 점에서 ‘as’보다 약간의 장점이 있지만, 그 외에는 동등합니다. 그러나 이는 Python의 다른->
사용(함수 반환 타입 어노테이션)과는 완전히 관련이 없으며,:=
(Algol-58로 거슬러 올라감)에 비해 훨씬 약한 전통을 가지고 있습니다.- 선행 점(leading dot)으로 구문-지역 이름 장식 (Adorning statement-local names with a leading dot):
stuff = [[(f(x) as .y), x/.y] for x in range(5)] # "as" 사용 stuff = [[(.y := f(x)), x/.y] for x in range(5)] # ":=" 사용
이것은 유출된 사용을 쉽게 감지할 수 있게 하여 일부 형태의 구문 모호성을 제거한다는 장점이 있습니다. 그러나 이는 Python에서 변수의 스코프가 이름에 인코딩되는 유일한 곳이 되어 리팩토링을 더 어렵게 만들 것입니다.
- 지역 이름 바인딩을 생성하기 위해 모든 구문에
where:
추가:value = x**2 + 2*x where: x = spam(1, 4, 7, q)
실행 순서가 반전됩니다 (들여쓰기된 본문이 먼저 수행되고, 이어서 “헤더”가 수행됩니다). 이것은 새로운 키워드를 필요로 합니다 (기존 키워드를 재사용하지 않는 한, 가장 가능성 있는 것은
with:
입니다). 이 주제에 대한 이전 논의(제안된 키워드가given:
이었음)는 PEP 3150을 참조하십시오. 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:
(유용할 수 있지만 혼란스러울 수도 있음)과 병렬을 이루는 대신, 이것은 병렬이 없지만 암시적입니다.
조건 구문 특별 처리 (Special-casing conditional statements)
가장 인기 있는 사용 사례 중 하나는 if
및 while
구문입니다. 더 일반적인 해결책 대신, 이 제안은 비교되는 값을 캡처하는 수단을 추가하기 위해 이 두 구문의 문법을 향상시킵니다:
if re.search(pat, text) as match:
print("Found:", match.group(0))
이것은 캡처된 값의 진실성(truthiness)에 따라 원하는 조건이 결정될 경우에만 효과적으로 작동합니다. 따라서 특정 사용 사례(정규식 매치, 완료 시 ''
를 반환하는 소켓 읽기)에는 효과적이지만, 더 복잡한 경우(예: 조건이 f(x) < 0
이고 f(x)
의 값을 캡처하려는 경우)에는 완전히 쓸모가 없습니다. 또한 리스트 컴프리헨션에는 이점이 없습니다.
- 장점: 구문 모호성이 없습니다.
- 단점:
if
/while
구문에서도 가능한 사용 사례의 극히 일부만을 해결합니다.
컴프리헨션 특별 처리 (Special-casing comprehensions)
또 다른 일반적인 사용 사례는 컴프리헨션(list
/set
/dict
, 그리고 genexps
)입니다. 위에서 언급했듯이, 컴프리헨션별 솔루션에 대한 제안이 있었습니다.
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
루프의 풀어서 쓰는 형태로 간단히 변환하면 의미론이 drastically 달라질 것입니다. 컴프리헨션 내부의with
의미는 동일한 구문을 유지하면서 독립적인 구문의 의미와 완전히 다를 것입니다.
어떤 표기법을 선택하든, 이는 컴프리헨션과 동등한 풀어서 쓰는 루프 형태 사이에 뚜렷한 차이를 도입합니다. 이름 바인딩을 재작업하지 않고는 루프를 구문 형태로 풀 수 없게 됩니다. 이 작업을 위해 재사용할 수 있는 유일한 키워드는 with
이므로, 컴프리헨션에서 구문과는 미묘하게 다른 의미론을 부여하게 됩니다. 또는 새로운 키워드가 필요하며, 그에 따른 모든 비용이 발생합니다.
연산자 우선순위 낮추기 (Lowering operator precedence)
:=
연산자에는 두 가지 논리적인 우선순위가 있습니다. 할당 구문처럼 가능한 한 느슨하게 바인딩되거나, 비교 연산자보다 더 강하게 바인딩되어야 합니다. 우선순위를 비교 및 산술 연산자 사이(정확히는 비트와이즈 OR보다 약간 낮게)에 두면 while
및 if
조건 내의 대부분의 사용이 괄호 없이 표기될 수 있습니다. 값을 캡처한 다음 비교를 수행하는 것이 가장 일반적이기 때문입니다:
pos = -1
while pos := buffer.find(search_term, pos + 1) >= 0:
...
find()
가 -1
을 반환하면 루프가 종료됩니다. :=
가 =
처럼 느슨하게 바인딩된다면, 이것은 비교의 결과(일반적으로 True
또는 False
)를 캡처할 것이며, 이는 덜 유용합니다.
이러한 동작이 많은 상황에서 편리하겠지만, “: =
연산자는 할당 구문과 동일하게 작동한다”는 것보다 설명하기 더 어렵습니다. 따라서 :=
의 우선순위는 =
와 가능한 한 가깝게 만들어졌습니다 (쉼표보다 더 강하게 바인딩된다는 예외는 있음).
우측에 쉼표 허용 (Allowing commas to the right)
일부 비판자들은 할당 표현식이 우측에 괄호 없는 튜플을 허용해야 하므로, 다음 두 가지가 동등해야 한다고 주장했습니다:
(point := (x, y))
(point := x, y)
(제안의 현재 버전에서는 후자는 ((point := x), y)
와 동등합니다).
그러나 이러한 입장을 채택한다면 함수 호출에서 사용될 때 할당 표현식도 쉼표보다 덜 강하게 바인딩된다는 결론으로 논리적으로 이어질 것이며, 따라서 다음과 같은 혼란스러운 동등성을 갖게 될 것입니다:
foo(x := 1, y)
foo(x := (1, y))
덜 혼란스러운 옵션은 :=
가 쉼표보다 더 강하게 바인딩되도록 하는 것입니다.
항상 괄호 요구 (Always requiring parentheses)
할당 표현식 주변에 항상 괄호를 요구하자는 제안이 있었습니다. 이것은 많은 모호성을 해결할 것이며, 실제로 원하는 하위 표현식을 추출하기 위해 괄호가 자주 필요할 것입니다. 그러나 다음 경우에는 추가 괄호가 불필요하게 느껴집니다:
# if 문의 최상위
if match := pattern.match(line):
return match.group(1)
# 짧은 호출
len(lines := f.readlines())
자주 제기되는 이의 (Frequently Raised Objections)
기존 할당을 표현식으로 바꾸지 않는 이유 (Why not just turn existing assignment into an expression?)
C 언어와 그 파생 언어는 =
연산자를 Python의 방식처럼 구문이 아닌 표현식으로 정의합니다. 이것은 비교가 더 흔한 컨텍스트를 포함하여 더 많은 컨텍스트에서 할당을 허용합니다. if (x == y)
와 if (x = y)
사이의 구문적 유사성은 그들의 drastically 다른 의미론을 숨깁니다. 따라서 이 제안은 :=
를 사용하여 이러한 구분을 명확히 합니다.
할당 표현식이 있는데 왜 할당 구문이 필요한가 (With assignment expressions, why bother with assignment statements?)
두 가지 형태는 다른 유연성을 가집니다. :=
연산자는 더 큰 표현식 내에서 사용될 수 있습니다. =
구문은 +=
및 그 친구들로 확장될 수 있고, 체인될 수 있으며, 속성(attributes) 및 첨자(subscripts)에 할당할 수 있습니다.
서브로컬 스코프(sublocal scope)를 사용하고 네임스페이스 오염을 방지하지 않는 이유 (Why not use a sublocal scope and prevent namespace pollution?)
이 제안의 이전 개정판은 서브로컬 스코프(단일 구문으로 제한됨)를 포함하여 이름 유출 및 네임스페이스 오염을 방지했습니다. 이는 여러 상황에서 확실한 장점이지만, 다른 많은 상황에서는 복잡성을 증가시키며, 그 이점이 비용을 정당화하지 못했습니다. 언어 단순성을 위해, 여기서 생성된 이름 바인딩은 다른 이름 바인딩과 정확히 동등하며, 클래스 또는 모듈 스코프에서의 사용은 외부에서 볼 수 있는 이름을 생성할 것입니다. 이는 for
루프 또는 다른 구성과 다르지 않으며, 동일한 방식으로 해결할 수 있습니다: 더 이상 필요하지 않으면 이름을 del
하거나 밑줄로 접두사를 붙입니다.
(저자는 이 방향으로 제안을 이동하도록 제안한 Alyssa Coghlan과 Steven D’Aprano에게 감사드립니다).
스타일 가이드 권장 사항 (Style guide recommendations)
표현식 할당이 때때로 구문 할당과 동등하게 사용될 수 있으므로, 어떤 것을 선호해야 하는지에 대한 질문이 생길 것입니다. PEP 8과 같은 스타일 가이드를 위해 두 가지 권장 사항이 제안됩니다.
- 할당 구문 또는 할당 표현식 중 하나를 사용할 수 있다면, 구문을 선호하십시오. 이는 의도를 명확하게 선언합니다.
- 할당 표현식을 사용하면 실행 순서에 대한 모호성이 발생할 경우, 대신 구문을 사용하도록 재구성하십시오.
감사 (Acknowledgements)
저자들은 이 제안에 상당한 기여를 해준 Alyssa Coghlan과 Steven D’Aprano, 그리고 구현 지원을 해준 core-mentorship 메일링 리스트 회원들에게 감사드립니다.
부록 A: Tim Peters의 발견 (Appendix A: Tim Peters’s findings)
다음은 Tim Peters가 이 주제에 대해 작성한 짧은 에세이입니다.
저는 “번잡한(busy)” 코드 줄을 싫어하며, 개념적으로 관련 없는 로직을 한 줄에 넣는 것도 싫어합니다. 예를 들어, 다음 대신:
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
테스트는 너무 미묘하며, 단락(short-circuiting) 또는 메서드 체인(method-chaining) 컨텍스트가 아닌 곳에서 엄격한 왼쪽에서 오른쪽 평가에 결정적으로 의존합니다. 제 두뇌는 그렇게 작동하지 않습니다.
하지만 그런 경우는 드물었습니다. 이름 바인딩은 매우 빈번하며, “희소한 것이 밀집한 것보다 낫다”는 “거의 비어 있는 것이 희소한 것보다 낫다”는 것을 의미하지 않습니다. 예를 들어, 저는 “이 경우에 유용한 것을 반환할 것이 없지만, 자주 예상되므로 예외로 귀찮게 하지 않겠다”는 의미로 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 numeric example)
당시 저를 꽤 감동시켰던 또 다른 예제가 있습니다.
모든 변수가 양의 정수이고 a
가 x
의 n승근 이상인 경우, 이 알고리즘은 x
의 n승근의 내림값(floor)을 반환합니다 (그리고 반복당 정확한 비트 수를 대략 두 배로 늘립니다):
while a > (d := x // a**(n-1)):
a = ((n-1)*a + d) // n
return a
이것이 작동하는 이유는 분명하지 않지만, “절반 루프” 형태에서도 더 분명하지 않습니다. 올바른 통찰( “산술 평균-기하 평균 부등식”)에 기반하지 않고, 중첩된 내림 함수가 어떻게 작동하는지에 대한 비자명적인 지식을 모른다면 정확성을 증명하기 어렵습니다. 즉, 도전 과제는 코딩이 아니라 수학에 있습니다.
이 모든 것을 알고 있다면, 할당 표현식 형태는 “현재 추정치가 너무 크면 더 작은 추정치를 얻는다”로 쉽게 읽을 수 있습니다. 여기서 “너무 큰가?” 테스트와 새로운 추정치는 비용이 많이 드는 하위 표현식을 공유합니다.
제 눈에는 원래 형태가 이해하기 더 어렵습니다:
while True:
d = x // a**(n-1)
if a <= d:
break
a = ((n-1)*a + d) // n
return a
부록 B: 컴프리헨션에 대한 대략적인 코드 번역 (Appendix B: Rough code translations for comprehensions)
이 부록은 대상이 컴프리헨션 또는 제너레이터 표현식에 나타날 때의 규칙을 명확히 하려고 시도합니다 (그러나 명시하지는 않습니다). 여러 예시를 위해 컴프리헨션을 포함하는 원래 코드와, 컴프리헨션이 동등한 제너레이터 함수와 일부 스캐폴딩으로 대체된 번역을 보여줍니다.
[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 # TARGET이 지역 변수임을 보장하는 죽은 코드
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)] # 즉, a = [[0, 1, 2], [0, 1, 2]]
print(TARGET) # 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: 스코프 의미론 변경 없음 (Appendix C: No Changes to Scope Semantics)
혼란스러웠던 점이므로, Python의 스코프 의미론에는 아무것도 변경되지 않았다는 점에 유의하십시오. 함수 지역 스코프는 컴파일 시간에 계속 해결되며, 런타임에는 무기한 시간적 범위를 가집니다 (“완전 클로저(full closures)”). 예시:
a = 42
def f():
# `a`는 `f`에 지역적이지만, 호출자가 이 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")
Then:
>>> results = list(f()) # [genexp, lambda]
done
`a` is not yet bound
# CPython에서 f의 실행 프레임은 더 이상 존재하지 않지만,
# f의 지역 변수는 여전히 참조될 수 있는 한 살아 있습니다.
>>> list(map(type, results))
[<class 'generator'>, <class 'function'>]
>>> list(results[0])
[0, 1, 2]
>>> results[1]()
102
>>> a
42
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments