Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이는 NAME := expr 표기법을 사용하여 표현식 내부에서 변수에 할당할 방법을 만드는 제안입니다.

이 변경의 일환으로 딕셔너리 컴프리헨션의 평가 순서도 업데이트하여, 값 표현식보다 키 표현식이 먼저 실행되도록 합니다. 이를 통해 키를 이름에 바인딩한 다음 해당 값을 계산하는 과정에서 다시 사용할 수 있습니다.

이 PEP에 관한 논의 중에 이 연산자는 비공식적으로 “왈러스 연산자”로 알려지게 되었습니다. 이 구문의 공식 명칭은 “Assignment Expressions”입니다(PEP 제목에 따름). 그러나 “Named Expressions”라고도 부를 수 있습니다(예를 들어 CPython 참조 구현에서는 내부적으로 이 이름을 사용합니다).

근거

표현식의 결과에 이름을 붙이는 것은 프로그래밍의 중요한 부분이며, 더 긴 표현식 대신 설명적인 이름을 사용할 수 있게 하고 재사용도 가능하게 합니다. 현재 이 기능은 문장 형식으로만 사용할 수 있으므로, 리스트 컴프리헨션 및 기타 표현식 컨텍스트에서는 사용할 수 없습니다.

또한 큰 표현식의 하위 부분에 이름을 붙이면 대화형 디버거를 지원하여 유용한 디스플레이 훅과 부분 결과를 제공할 수 있습니다. 하위 표현식을 인라인으로 캡처할 방법이 없다면 원래 코드를 리팩터링해야 하지만, 할당 표현식을 사용하면 몇 개의 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 표현식을 사용할 수 있는 대부분의 컨텍스트에서는 named expression이 나타날 수 있습니다. 이는 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() 표현식에 대한 “증거”나 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 ...}에서는 현재 YX보다 먼저 평가됩니다. 이를 변경하여 XY보다 먼저 평가되도록 제안합니다. ({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를 발생시킵니다. 이는 명명 표현식의 대상이 컴프리헨션의 반복 변수 중 하나와 충돌하는 경우에만 발생시키는 방식이 아닙니다. 충분히 설득력 있는 예제가 있다면 이를 재검토할 수 있지만, 더 선택적인 제한을 구현하는 데 필요한 추가 복잡성은 순전히 가상의 사용 사례를 위해 감수할 가치가 없어 보입니다.

예제

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

tzs += 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 루프에서는 무한 루프와 할당 및 조건을 따로 둘 필요가 없어집니다. 또한 함수 호출을 조건으로만 사용하는 루프와, 해당 함수 호출을 조건으로 사용하면서 실제 값도 사용하는 루프 사이에 자연스러운 대응 관계를 만듭니다.

포크

저수준 UNIX 세계의 예:

if pid := os.fork():
    # Parent code
else:
    # Child code

거부된 대안 제안

이 제안과 대체로 유사한 제안은 python-ideas에서 자주 제기되었습니다. 아래에는 여러 대체 구문이 제시되어 있으며, 그중 일부는 컴프리헨션에만 해당하고 위에서 제시한 구문을 채택하면서 거부되었습니다.

컴프리헨션의 스코프 규칙 변경

이 PEP의 이전 버전에서는 컴프리헨션의 스코프 규칙을 미묘하게 변경하여 클래스 스코프에서 더 유용하게 만들고, “가장 바깥쪽 이터러블”의 스코프와 컴프리헨션의 나머지 부분의 스코프를 통일하자고 제안했습니다. 그러나 이 제안의 해당 부분은 하위 호환성 문제를 일으켰을 것이므로 철회되었으며, 이제 PEP는 할당 표현식에 집중할 수 있습니다.

대체 표기

현재 제안과 대체로 동일한 의미를 가지지만 다르게 표기합니다.

  1. EXPR as NAME:
    stuff = [[f(x) as y, x/y] for x in range(5)]
    

    EXPR as NAME은 이미 import, exceptwith 문에서 서로 다른 의미로 사용되고 있으므로, 이를 사용하면 불필요한 혼란이 생기거나 특별한 예외 처리가 필요합니다(예: 이러한 문의 헤더 내부에서 할당을 금지해야 합니다).

    (with EXPR as VAREXPR의 값을 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

      대입 표현식을 시각적으로 인식하는 데 도움이 됩니다.

  2. EXPR -> NAME:
    stuff = [[f(x) -> y, x/y] for x in range(5)]
    

    이 구문은 R과 Haskell 같은 언어 및 일부 프로그래밍 가능한 계산기에서 영감을 받았습니다. (왼쪽을 향하는 화살표인 y <- f(x)는 Python에서 사용할 수 없습니다. 작음 연산자와 단항 뺄셈으로 해석되기 때문입니다.) 이 구문은 with, exceptimport와 충돌하지 않는다는 점에서 ‘as’보다 약간 유리하지만, 그 외에는 동등합니다. 그러나 이는 Python에서 ->를 사용하는 다른 경우인 함수 반환 타입 어노테이션과는 전혀 관련이 없으며, :=(Algol-58까지 거슬러 올라갑니다)와 비교하면 전통이 훨씬 약합니다.

  3. 문장 지역 이름 앞에 선행 점을 붙이기:
    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에서 변수의 스코프가 변수 이름에 인코딩되는 유일한 경우가 되므로 리팩터링이 더 어려워집니다.

  4. 지역 이름 바인딩을 생성하기 위해 모든 문에 where:를 추가하기:
    value = x**2 + 2*x where:
        x = spam(1, 4, 7, q)
    

    실행 순서가 뒤집힙니다 (들여쓰기된 본문이 먼저 실행되고, 그다음에 “헤더”가 실행됩니다). 기존 키워드를 다른 용도로 사용하지 않는 한 새 키워드가 필요합니다 (가장 가능성이 높은 것은 with:입니다). 이 주제에 관한 이전 논의는 PEP 3150을 참조하십시오 (제안된 키워드는 given:입니다).

  5. TARGET from EXPR:
    stuff = [[y from f(x), x/y] for x in range(5)]
    

    이 구문은 as보다 충돌이 적고 (raise Exc from Exc 표기법과만 충돌하지만), 그 외에는 이와 비슷합니다. with expr as target:와 병렬을 이루는 대신, 이 구문에는 병렬 관계가 없지만 연상을 불러일으킵니다.

조건문에 대한 특수 처리

가장 인기 있는 사용 사례 중 하나는 ifwhile 문입니다. 더 일반적인 해결책 대신, 이 제안은 이 두 문의 구문을 확장하여 비교된 값을 캡처할 수단을 추가합니다:

if re.search(pat, text) as match:
    print("Found:", match.group(0))

원하는 조건이 캡처된 값의 진릿값을 기반으로 하는 경우에만, 그리고 오직 그런 경우에만 매우 잘 작동합니다. 따라서 특정 사용 사례(정규식 일치, 완료 시 ''를 반환하는 소켓 읽기)에는 효과적이지만, 더 복잡한 경우(예를 들어 조건이 f(x) < 0이고 f(x)의 값을 캡처하려는 경우)에는 완전히 쓸모가 없습니다. 리스트 컴프리헨션에는 아무런 이점도 없습니다.

장점: 구문상의 모호성이 없습니다. 단점: if/while 문에서도 가능한 사용 사례 중 일부만 다룹니다.

컴프리헨션의 특수 처리

또 다른 일반적인 사용 사례는 컴프리헨션(리스트/집합/딕셔너리 및 제너레이터 표현식)입니다. 위와 같이 컴프리헨션별 해결책에 대한 제안이 제시되었습니다.

  1. 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도 마찬가지입니다).

  2. with NAME = EXPR:
    stuff = [(y, x/y) with y = f(x) for x in range(5)]
    

    위와 같지만 with 키워드를 재사용합니다. 읽기에도 크게 나쁘지 않으며, 추가 언어 키워드가 필요하지 않습니다. 다만 컴프리헨션으로 제한되며, “장황한” for-루프 구문으로 쉽게 변환할 수 없습니다. 표현식의 등호가 이제 비교를 수행하는 대신 이름 바인딩을 만들 수 있다는 C의 문제가 있습니다. “with NAME = EXPR:”을 단독 문으로 사용할 수 없는 이유에 대한 의문이 제기될 것입니다.

  3. 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보다 바로 낮은 위치)에 두면 whileif 조건 안에서 대부분의 사용 사례를 괄호 없이 표현할 수 있습니다. 무언가의 값을 저장한 다음 그 값에 대해 비교를 수행하려는 경우가 가장 많기 때문입니다:

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의 방식처럼 명령문이 아니라 표현식으로 = 연산자를 정의합니다. 이를 통해 대입을 더 많은 문맥에서 사용할 수 있으며, 비교가 더 일반적인 문맥도 포함됩니다. if (x == y)if (x = y) 사이의 문법적 유사성은 두 표현식의 의미가 극적으로 다르다는 사실을 가립니다. 따라서 이 제안에서는 차이를 명확히 하기 위해 :=를 사용합니다.

대입 표현식이 있다면 대입 명령문은 왜 사용합니까?

두 형식은 서로 다른 유연성을 가집니다. := 연산자는 더 큰 표현식 내부에서 사용할 수 있지만, = 명령문은 += 및 그와 유사한 연산자로 확장할 수 있고, 연쇄할 수 있으며, 속성과 첨자에 대입할 수 있습니다.

하위 지역 범위를 사용하여 네임스페이스 오염을 방지하면 안 됩니까?

이 제안의 이전 개정안에서는 하위 지역 범위(단일 명령문으로 제한됨)를 사용하여 이름 누출과 네임스페이스 오염을 방지했습니다. 이는 여러 상황에서 분명한 장점이지만, 다른 많은 상황에서는 복잡성을 증가시키며, 그 비용은 이점으로 정당화되지 않습니다. 언어를 단순하게 유지하기 위해 여기서 생성되는 이름 바인딩은 다른 모든 이름 바인딩과 정확히 동일하며, 클래스 또는 모듈 범위에서 사용하면 외부에서 볼 수 있는 이름이 생성된다는 점도 마찬가지입니다. 이는 for 루프나 다른 구성 요소와 다르지 않으며, 같은 방식으로 해결할 수 있습니다. 더 이상 필요하지 않을 때 del로 이름을 삭제하거나, 이름 앞에 밑줄을 붙이면 됩니다.

(저자는 이 제안을 이러한 방향으로 발전시키자고 제안해 주신 Guido van Rossum과 Christoph Groth에게 감사의 뜻을 전하고자 합니다. [2])

스타일 가이드 권장 사항

표현식 대입은 때때로 명령문 대입과 동일하게 사용할 수 있으므로, 어느 쪽을 선호해야 하는지에 대한 질문이 제기됩니다. 관련 PEP 8과 같은 스타일 가이드에 도움이 되도록 두 가지 권고 사항을 제안합니다.

  1. 대입 명령문과 대입 표현식 중 어느 쪽이든 사용할 수 있다면 명령문을 선호하십시오. 명령문은 의도를 명확하게 드러냅니다.
  2. 대입 표현식을 사용하면 실행 순서가 모호해지는 경우에는 대신 명령문을 사용하도록 구조를 변경하십시오.

감사의 말

저자들은 이 제안에 크게 기여해 주신 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

화면에 주변 코드의 _line_을 하나 더 표시하기 위해 약간의 가로 공백을 포기하는 것도 좋습니다. 처음에는 이를 중요하게 여기지 않았지만, 그 일이 워낙 자주 발생하다 보니 효과가 누적되었고, 얼마 지나지 않아 더 간결한 코드를 실제로 실행할 수 없다는 사실에 짜증이 나기 시작했습니다. 이는 놀라운 일이었습니다!

대입 표현식이 정말 빛을 발하는 다른 경우들도 있습니다. 제 코드에서 다른 예를 고르는 대신, 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번째 제곱근의 바닥값을 반환합니다(그리고 반복할 때마다 정확한 비트 수가 대략 두 배로 늘어납니다).:

while a > (d := x // a**(n-1)):
    a = ((n-1)*a + d) // n
return a

그것이 왜 작동하는지는 명확하지 않지만, “loop and a half” 형태에서도 더 명확하지는 않습니다. 올바른 통찰(“산술 평균 - 기하 평균 부등식”)을 바탕으로 하지 않고는, 그리고 중첩된 바닥 함수(floor function)가 어떻게 동작하는지에 대한 자명하지 않은 몇 가지 사실을 알지 못하고는 정확성을 증명하기 어렵습니다. 즉, 어려움은 수학에 있는 것이지 코딩에 있는 것이 아닙니다.

그 모든 것을 알고 있다면, 대입 표현식 형태는 “현재 추측값이 너무 크면 더 작은 추측값을 구하라”는 의미로 쉽게 읽을 수 있으며, 여기서 “너무 큰가?” 검사와 새 추측값은 비용이 큰 하위 표현식을 공유합니다.

제 눈에는 원래 형태가 이해하기 더 어렵습니다:

while True:
    d = x // a**(n-1)
    if a <= d:
        break
    a = ((n-1)*a + d) // n
return a

부록 B: 컴프리헨션에 대한 대략적인 코드 변환

이 부록에서는 대상(target)이 컴프리헨션이나 제너레이터 표현식에 나타날 때의 규칙을 (명시하지는 않지만) 명확히 하고자 합니다. 여러 예시를 통해 컴프리헨션을 포함한 원본 코드와, 그 컴프리헨션을 동등한 제너레이터 함수와 약간의 보조 장치(scaffolding)로 대체한 변환 코드를 보여줍니다.

[x for ...]list(x for ...)와 동등하므로, 이 예시들은 일반성을 잃지 않고 모두 리스트 컴프리헨션을 사용합니다. 그리고 이 예시들은 규칙의 경계 사례(edge case)를 명확히 하기 위한 것이므로, 실제 코드처럼 보이려고 하지 않습니다.

참고: 컴프리헨션은 이미 이 부록에 나오는 것과 같은 중첩된 제너레이터 함수를 합성하는 방식으로 구현되어 있습니다. 새로 추가되는 부분은 대입 표현식 대상의 의도된 스코프(가장 바깥쪽 컴프리헨션을 포함하는 블록에서 대입이 수행되었을 때와 동일한 스코프로 결정됨)를 확립하기 위해 적절한 선언을 추가하는 것입니다. 타입 추론 목적으로 볼 때, 이러한 예시적 전개(expansion)는 대입 표현식 대상이 항상 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

참고 자료