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

Python 개선 제안 한국어 번역

PEP 577 – 증강 대입 표현식

Author:
Alyssa Coghlan <ncoghlan at gmail.com>
Status:
Withdrawn
Type:
Standards Track
Created:
14-May-2018
Python-Version:
3.8
Post-History:
22-May-2018

Table of Contents

번역·라이선스 안내

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

PEP 철회

이 PEP를 작성하면서, 이전에 참조되지 않은 대입 대상에 대한 PEP 572의 제안된 스코핑 규칙에서 실제로 저를 괴롭히던 문제가 무엇인지 이 PEP가 제대로 다루지 못한다는 사실을 깨달았으며, 상당히 바람직하지 않은 결과도 초래한다는 사실을 깨달았습니다(가장 두드러진 문제는 >>=<<=를 인라인 증강 대입 연산자로 허용하는 것인데, 이는 >=<= 비교 연산자와는 전혀 다른 의미를 가집니다).

또한 고유한 문법이 없더라도 PEP 572는 기술적으로 operator 모듈을 사용하여 인라인 증강 대입을 작성하는 것을 허용한다는 사실도 깨달았습니다.:

from operator import iadd
if (target := iadd(target, value)) < limit:
    ...

인라인 대입 대상에 단순 이름만 허용하도록 제한하면 대상 표현식을 부작용 없이 항상 반복할 수 있으므로, 실제로 삽입된 증강 대입을 허용할 때 발생할 모호성을 피할 수 있습니다(사람이 읽기에는 거의 확실히 어려울 것이므로 여전히 나쁜 생각이지만, 이 주석은 언어 수준의 표현력에 대한 이론적 한계만을 다룹니다).

따라서 저는 이 PEP를 공표 요청 없이 철회했습니다. 당시 저는 현재 스코프에서 아직 지역 변수로 선언되지 않은 대입 대상의 처리에만 초점을 맞춘 대체 PEP도 작성하기 시작했습니다(일반 블록 스코프와 스코프가 지정된 표현식 모두에 대해). 그러나 그 초안은 결국 PEP 572에서 채택된 제안보다 제가 더 좋다고 생각하는 단계에조차 도달하지 못했으므로, 어디에도 게시되지 않았고 PEP 번호도 할당받지 못했습니다.

초록

이는 x += 1과 같은 증강 대입을 문장뿐만 아니라 표현식으로도 사용할 수 있도록 하자는 제안입니다.

이와 함께 NAME := EXPR은 기존 이름 바인딩 문장이 하는 것처럼 새 지역 변수 이름을 암시적으로 정의하는 대신, 새로운 증강 대입 스코핑 규칙을 사용하는 인라인 대입 표현식으로 제안됩니다. 함수 스코프에서 표현식 수준의 지역 변수 선언을 허용할지는 표현식 수준의 이름 바인딩을 허용할지에 대한 문제와 의도적으로 분리하며, 이후 PEP로 미룹니다.

이 PEP는 PEP 572의 직접적인 경쟁 제안입니다(다만 해당 PEP의 동기를 많이 차용했으며 인라인 대입에 대해 제안된 문법도 공유합니다). 두 PEP의 연관 관계에 대한 자세한 내용은 Relationship with PEP 572를 참조하십시오.

새로운 표현식의 사용성을 높이기 위해, 일반 블록 스코프(모듈, 클래스 및 함수)에서의 증강 대입 처리와 스코프가 지정된 표현식(람다 표현식, 제너레이터 표현식 및 컴프리헨션)에서의 증강 대입 처리를 의미론적으로 분리하여, 모든 인라인 대입이 가장 가까운 포함 블록 스코프를 대상으로 하도록 기본 설정하는 방안을 제안합니다.

인라인 대입으로 어떤 대상을 다시 바인딩해야 하는지가 현재 불분명하다고 판단되는 경우 또는 다른 이유로 인라인 대입의 대상 스코프가 유효하지 않다고 판단되는 경우를 처리하기 위해, SyntaxError의 서브클래스로 새로운 컴파일 시간 TargetNameError를 추가합니다.

문법 및 의미론

증강 대입 표현식

증강 대입이 표현식으로 나타날 수 있도록 언어 문법을 조정하며, 증강 대입 표현식의 결과는 주어진 대상에 바인딩되는 계산 후 참조와 동일합니다.

예를 들어:

>>> n = 0
>>> n += 5
5
>>> n -= 2
3
>>> n *= 3
9
>>> n
9

변경 가능한 대상의 경우, 이는 결과가 항상 원래 객체 자체임을 의미합니다.:

>>> seq = []
>>> seq_id = id(seq)
>>> seq += range(3)
[0, 1, 2]
>>> seq_id == id(seq)
True

속성 및 컨테이너 첨자에 대한 증강 대입을 허용하며, 결과는 단순 이름 대상에서와 마찬가지로 대상에 바인딩되는 계산 후 참조가 됩니다.:

def increment(self, step=1):
    return self._value += step

이 경우에는 할당이 이미 수행된 후 __getitem____getattribute__호출되지 않으며, 인플레이스 연산을 평가하는 데 필요한 경우에만 호출됩니다.

인라인 대입 연산자 추가

증강 대입 표현식만 추가할 경우, 다음과 같이 동작하는 Target 래퍼 형식을 정의하여 |=와 같은 기호를 범용 대입 연산자로 악용할 수 있습니다.:

>>> class Target:
...     def __init__(self, value):
...         self.value = value
...     def __or__(self, other):
...         return Target(other)
...
>>> x = Target(10)
>>> x.value
10
>>> x |= 42
<__main__.Target object at 0x7f608caa8048>
>>> x.value
42

이는 nonlocal 키워드가 없을 때 단일 참조를 리스트에 저장하는 방법이 오랫동안 해결책으로 사용되었던 것과 유사하며, 오늘날에도 (operator.itemsetter와 함께) 표현식 수준 대입이 없는 문제를 우회하는 데 사용할 수 있습니다.

이러한 우회 방법을 요구하는 대신, 이 PEP는 아래에서 설명하는 증강 대입 스코핑 규칙을 사용하는 새로운 인라인 대입 표현식으로 PEP 572의 “NAME := EXPR” 문법을 채택하자고 제안합니다.

이는 새 값만 중요하고 이전에 바인딩된 값이 있다면 이를 완전히 폐기해도 되는 경우를 깔끔하게 처리합니다.

단순한 이름과 복잡한 할당 대상 모두에서 인라인 할당 연산자는 새 참조를 할당하기 전에 이전 참조를 읽지 않습니다. 그러나 함수 스코프에서 사용될 때(직접 사용하거나 스코프가 지정된 표현식 내부에서 사용하거나) 이는 새로운 지역 변수를 암묵적으로 정의하지 않으며, 대신 TargetNameError를 발생시킵니다(아래의 증강 대입에서 설명한 것과 같습니다).

대입 연산자 우선순위

기존 증강 대입 문의 의미론을 보존하기 위해, 인라인 대입 연산자는 쉼표 의사 연산자를 포함한 다른 모든 연산자보다 낮은 우선순위를 갖도록 정의합니다. 이를 통해 최상위 표현식으로 사용할 때 처리할 값이 우변 표현식 전체로 계속 해석됩니다(그 값이 괄호 없는 튜플인 경우에도 마찬가지입니다).

이 PEP이 PEP 572와 비교하여 도입하는 차이점은 PEP 572에서는 (n := first, second)n = first로 설정하지만, 이 PEP에서는 n = (first, second)로 설정하며, 첫 번째 의미를 얻으려면 괄호 한 쌍을 추가해야 한다는 것입니다(((n := first), second)).

PEP 572에서는 대입 표현식을 함수 호출 인자로 사용할 때 이로 인해 모호성이 발생한다고 상당히 합리적으로 지적합니다. 이 PEP에서는 함수 호출의 인자로 사용되는 대입 표현식을 괄호로 묶도록 요구하는 방식으로 이 문제를 다르게 해결합니다(단독 인자인 경우는 제외합니다).

이는 제너레이터 표현식에 적용되는 제한을 완화한 버전입니다(제너레이터 표현식은 함수 호출의 단독 인자인 경우를 제외하면 항상 괄호가 필요합니다).

블록 스코프의 이름에 대한 증강 대입

모듈 또는 클래스 스코프의 증강 할당에 대해서는 대상 이름 바인딩 변경을 제안하지 않습니다(여기에는 “exec” 또는 “eval”을 사용하여 실행되는 코드도 포함됩니다). 이러한 경우에도 현재와 마찬가지로 대입 대상을 새 지역 변수로 암묵적으로 선언하며, 필요한 경우 이름을 지역에 바인딩하기 전에 외부 스코프에서 확인할 수 있습니다.

함수 스코프에서는 증강 대입이 대상 이름을 함수에 지역적인 이름으로 명시적으로 설정하도록 앞선 이름 바인딩 또는 변수 선언을 요구하거나, 명시적인 global또는 nonlocal선언을 요구하도록 변경합니다. 이러한 바인딩이나 선언이 없으면 새로운 SyntaxError하위 클래스인 TargetNameError가 컴파일 시점에 발생합니다.

예를 들어 다음 코드는 현재와 동일하게 컴파일되고 실행됩니다.:

x = 0
x += 1 # Sets global "x" to 1

class C:
    x += 1 # Sets local "x" to 2, leaves global "x" alone

def local_target():
    x = 0
    x += 1 # Sets local "x" to 1, leaves global "x" alone

def global_target():
    global x
    x += 1 # Increments global "x" each time this runs

def nonlocal_target():
    x = 0
    def g():
        nonlocal x
        x += 1 # Increments "x" in outer scope each time this runs
        return x
    return g

다음 예제들은 모두 여전히 컴파일된 후 현재와 마찬가지로 런타임에 오류를 발생시킵니다.:

n += 1 # Raises NameError at runtime

class C:
    n += 1 # Raises NameError at runtime

def missing_global():
    global n
    n += 1 # Raises NameError at runtime

def delayed_nonlocal_initialisation():
    def f():
        nonlocal n
        n += 1
    f() # Raises NameError at runtime
    n = 0

def skipped_conditional_initialisation():
    if False:
        n = 0
    n += 1 # Raises UnboundLocalError at runtime

def local_declaration_without_initial_assignment():
    n: typing.Any
    n += 1 # Raises UnboundLocalError at runtime

반면 다음 코드는 처음에는 컴파일 시점에 DeprecationWarning을 발생시키고, 결국에는 컴파일 시점에 TargetNameError를 보고하도록 변경됩니다.:

def missing_target():
    x += 1 # Compile time TargetNameError due to ambiguous target scope
           # Is there a missing initialisation of "x" here? Or a missing
           # global or nonlocal declaration?

보수적인 구현 방식으로는 컴파일 시점의 함수 이름 확인 변경을 Python 3.8에서 DeprecationWarning으로 도입한 다음, Python 3.9에서 TargetNameError로 변환합니다. 이렇게 하면 사용되지 않는 함수가 실제로 호출될 경우에만 현재 UnboundLocalError를 발생시킬 수 있지만 실제로는 코드가 사용되지 않는 상황에서 발생할 수 있는 잠재적인 문제를 피할 수 있으며, 이러한 잠재된 런타임 결함을 컴파일 시점 오류로 변환하는 것은 폐기 기간이 필요한 하위 호환성이 깨지는 변경에 해당합니다.

함수 스코프에서 증강 대입을 독립 문장이 아니라 표현식으로 사용할 때는 하위 호환성 문제가 없으므로 컴파일 시점 이름 바인딩 검사를 Python 3.8에서 즉시 적용합니다.

마찬가지로 새로운 인라인 대입 표현식은 함수의 일부로 사용될 때 적어도 Python 3.8에서는 대상 스코프를 명시적으로 사전 선언해야 합니다. (향후 이 제한을 재검토할 가능성에 관한 메모는 설계 논의 절을 참조하십시오.)

스코프 표현식의 이름에 대한 증강 대입

스코프 표현식은 새로운 중첩 실행 스코프를 도입하는 표현식을 가리키기 위해 제안된 새로운 총칭입니다. 이러한 표현식은 연산의 본질적인 일부로서 새로운 스코프를 도입하거나(람다 표현식, 제너레이터 표현식), 또는 이름 바인딩 연산을 포함하는 스코프에서 숨기는 방식으로 새로운 스코프를 도입합니다(컨테이너 컴프리헨션).

일반 함수와 달리 이러한 스코프 표현식에는 외부 스코프의 이름을 직접 다시 바인딩하기 위한 명시적인 global또는 nonlocal선언을 포함할 수 없습니다.

대신 증강 대입 표현식에 대한 이러한 표현식의 이름 바인딩 의미는 다음과 같이 정의합니다.

  • 스코프 표현식에서 사용되는 증강 대입 대상은 포함하는 블록 스코프에 이미 바인딩되어 있거나, 해당 블록 스코프에서 그 스코프가 명시적으로 선언되어 있어야 합니다. 해당 스코프에서 적절한 이름 바인딩이나 선언을 찾을 수 없으면 스코프 표현식 내부에 새 바인딩을 만드는 대신 컴파일 시점에 TargetNameError를 발생시킵니다.
  • 포함하는 블록 스코프가 함수 스코프이고 대상 이름이 global또는 nonlocal로 명시적으로 선언된 경우, 스코프 표현식의 본문에서도 동일한 스코프 선언을 사용합니다.
  • 포함하는 블록 스코프가 함수 스코프이고 대상 이름이 해당 함수의 지역 변수인 경우, 스코프 표현식의 본문에서 암묵적으로 nonlocal로 선언합니다.
  • 포함하는 블록 스코프가 클래스 스코프인 경우에는 항상 TargetNameError를 발생시키며, 클래스 스코프와 스코프 표현식의 증강 대입을 결합하는 것이 현재 허용되지 않는다는 전용 메시지를 표시합니다.
  • 이름이 형식 매개변수(람다 표현식) 또는 반복 변수(제너레이터 표현식, 컴프리헨션)로 선언된 경우 해당 이름은 그 스코프 표현식에 지역적인 것으로 간주하며, 해당 스코프 또는 중첩된 스코프 표현식에서 이를 증강 대입 연산의 대상으로 사용하려고 하면 TargetNameError를 발생시킵니다(이는 나중에 해제될 가능성이 있는 제한이지만, 언어 참조에서 다루고 컴파일러와 인터프리터에서 처리해야 하는 초기 컴파일 시점 및 런타임 의미론을 단순화하기 위해 현재 제안하는 것입니다).

예를 들어 다음 코드는 예시와 같이 작동합니다.:

>>> global_target = 0
>>> incr_global_target = lambda: global_target += 1
>>> incr_global_target()
1
>>> incr_global_target()
2
>>> global_target
2
>>> def cumulative_sums(data, start=0)
...    total = start
...    yield from (total += value for value in data)
...    return total
...
>>> print(list(cumulative_sums(range(5))))
[0, 1, 3, 6, 10]

반면 다음 예제들은 모두 TargetNameError를 발생시킵니다.:

class C:
    cls_target = 0
    incr_cls_target = lambda: cls_target += 1 # Error due to class scope

def missing_target():
    incr_x = lambda: x += 1 # Error due to missing target "x"

def late_target():
    incr_x = lambda: x += 1 # Error due to "x" being declared after use
    x = 1

lambda arg: arg += 1 # Error due to attempt to target formal parameter

[x += 1 for x in data] # Error due to attempt to target iteration variable

현재 증강 대입은 스코프 표현식 내부에 나타날 수 없으므로, 위의 컴파일 시점 이름 확인 예외는 하위 호환성이 깨지는 변경으로서 단계적으로 도입할 필요 없이 초기 구현의 일부로 포함합니다.

설계 논의

복합 대입 대상 허용

이 PEP의 초기 초안에서는 증강 대입을 표현식으로 사용할 때 PEP 572의 단일 이름 대상 제한을 유지하고, 속성 및 첨자 대상을 문장 형식에서만 허용했습니다.

그러나 이를 적용하려면 증강 대입이 최상위 표현식인지 여부에 따라 허용되는 대상을 달리해야 했으며, n += 1, (n += 1), self.n += 1은 모두 허용되지만 (self.n += 1)은 금지되는 이유도 설명해야 했습니다. 따라서 표현식 형식에서도 기존의 모든 증강 대입 대상을 허용하도록 제안을 단순화했습니다.

이 PEP는 TARGET := EXPR을 증강 대입의 변형으로 정의하므로, 대입 대상과 첨자 대상도 지원하게 되었습니다.

증강 대입만인가, 아니면 이름 바인딩도 포함하는가?

PEP 572는 인라인 증강 대입의 잠재적 사용 사례가 일반적인 인라인 대입의 사용 사례보다 현저히 약하다는 타당한 근거를 제시하므로, 이를 x := x + 1로 표기하도록 요구하고 제자리 증강 대입 메서드는 사용하지 않도록 하는 것이 허용됩니다.

이는 적어도 내장 타입에 대해서는 어느 정도 사실이라고 할 수 있지만(잠재적인 반례는 PEP 작성자가 개인적으로 보유하지 않은 집합 조작 사용 사례에 초점을 맞춰야 할 가능성이 높습니다), 제자리 연산의 대안으로 사용하기 어려울 정도로 데이터 복사가 발생할 수 있는 NumPy 배열 조작과 같이 메모리를 더 많이 사용하는 사용 사례도 배제하게 됩니다.

그렇다고 해도 이 PEP가 주로 존재하는 이유는 PEP 작성자가 인라인 대입 제안을 “+=와 같지만, 덧셈 단계만 생략한 것”으로 이해하는 것이 훨씬 쉽다고 생각했기 때문이며, 그러한 관점이 함수 스코프에서 NAME = EXPRNAME := EXPR 사이에 실제 의미상의 차이를 제공한다는 점도 마음에 들었기 때문입니다.

이러한 대입 대상의 스코프 지정 동작 차이로 인해 NAME := EXPR 구문에는 다음 두 가지 주요 사용 사례가 있을 것으로 예상됩니다.

  • if 또는 while 문에서, 혹은 스코프가 지정된 표현식의 일부로 대입을 표현식 안에 삽입할 수 있게 하는 방법
  • 대입 대상 이름이 현재 함수 스코프에서 이전에 선언되었거나 바인딩되었는지 컴파일 시간에 검사하도록 요청하는 방법

모듈 또는 클래스 스코프에서는 컴파일러가 런타임에 해석 가능한 이름 집합을 볼 수 없으므로 NAME = EXPRNAME := EXPR이 의미상 동등하지만, 코드 린터와 정적 타입 검사기는 함수 스코프에서 컴파일러가 적용하는 것과 동일하게 NAME := EXPR에 대해 “사용 전에 선언 또는 대입이 필요함” 동작을 적용하도록 권장됩니다.

표현식 수준 대입 대상 선언에 대한 결정 연기

적어도 Python 3.8에서는 스코프가 지정된 표현식 밖에서 사용되는 경우에도, 함수 스코프의 인라인 대입(증강 대입 여부와 관계없이)을 사용하려면 TargetNameError가 발생하지 않도록 항상 앞선 이름 바인딩 또는 스코프 선언이 필요합니다.

이 요구 사항의 의도는 다음 두 가지 언어 설계 질문을 명확히 분리하는 것입니다.

  1. 표현식이 현재 스코프에서 이름을 다시 바인딩할 수 있는가?
  2. 표현식이 현재 스코프에서 새 이름을 선언할 수 있는가?

모듈 전역 스코프에서는 두 질문에 대한 답이 모두 명백히 “예”입니다. globals() 딕셔너리를 변경하면 런타임 모듈 스코프에 즉시 영향을 준다는 것이 언어 수준에서 보장되며, 함수 내부의 global NAME선언도 동일한 효과를 낼 수 있기 때문입니다(현재 실행 중인 모듈을 가져와 그 속성을 수정하는 경우도 마찬가지입니다).

클래스 스코프에서도 실제로는 두 질문에 대한 답이 모두 “예”이지만, locals()의 의미가 현재 공식적으로 명시되지 않았으므로 그 정도가 덜 명백합니다. 그러나 PEP 558이 제안하는 것처럼 클래스 스코프에서의 locals() 현재 동작을 규범적인 것으로 받아들인다면, 이는 locals()를 사용하는 것만 다를 뿐 모듈 전역을 조작하는 경우와 본질적으로 동일한 상황입니다.

하지만 함수 스코프에서는 이 두 질문에 대한 현재 답이 각각 “예”와 “아니요”입니다. 함수 지역 변수의 표현식 수준 재바인딩은 어휘적으로 중첩된 스코프와 명시적인 nonlocal NAME 표현식 덕분에 이미 가능합니다. 이 PEP는 표현식 수준 재바인딩을 현재보다 더 일반적으로 만들 가능성이 높지만, 이는 언어에 근본적으로 새로운 개념은 아닙니다.

반대로 함수 지역 변수를 선언하는 것은 현재 다음 중 하나를 포함하는 문 수준의 동작입니다.

  • 대입문(NAME = EXPR, OTHER_TARGET = NAME = EXPR 등)
  • 변수 선언(NAME : EXPR)
  • 중첩 함수 정의
  • 중첩 클래스 정의
  • for 루프
  • with
  • 접근 범위가 제한된 except

이 언어의 역사적 추세는 실제로 함수 로컬 이름의 표현식 수준 선언에 대한 지원을 제거하는 것이었습니다. 처음에는 “fast locals” 의미론이 도입되면서 함수 스코프에서 locals()를 통한 이름 도입이 지원되지 않게 되었고, 다시 Python 3.0에서 컴프리헨션 반복 변수를 숨기면서 이러한 추세가 이어졌습니다.

이제 Python 3.9에서는 Python 3.8의 표현식 수준 이름 바인딩 경험을 바탕으로 이 질문을 다시 검토하고, 표현식 수준의 함수 지역 변수 선언도 정말 원하며 NAME := EXPR을 이를 표기하는 방법으로 사용하기로 결정할 수도 있습니다(예를 들어 인라인 선언을 NAME := EXPR given NAME처럼 더 명시적으로 표기하는 대신 말입니다. 그렇게 하면 해당 선언에 타입 어노테이션을 포함할 수 있고, 포함하는 스코프의 네임스페이스를 오염시키지 않고 스코프가 지정된 표현식에서 새 지역 변수를 선언할 수도 있습니다).

그러나 이 PEP의 제안은 이 기능을 얼마나 원하는지, 그리고 정확히 어떤 부분에서 기능의 부재를 불편하게 느끼는지 결정할 수 있도록 한 번의 전체 릴리스 기간을 명시적으로 우리 자신에게 부여하자는 것입니다. Python은 수십 년 동안 표현식 수준의 이름 바인딩 또는 선언 없이도 문제없이 존속해 왔으므로, 우리가 그 두 가지를 모두 정말 원하는지 아니면 표현식 수준의 바인딩만으로 충분한지 결정하는 데 몇 년 정도를 할애할 여유가 있습니다.

증강 대입 대상을 결정할 때 스코프 표현 무시하기

관련 PEP 572의 대입 표현식에 가능한 바인딩 의미론을 논의하면서 Tim Peters는 중간에 있는 스코프 지정 표현식을 사실상 모두 무시하고 대입 표현식의 대상이 이를 포함하는 블록 스코프가 되어야 한다는 타당한 논거를 제시했습니다 [1], [2], [3].

이 접근 방식을 사용하면 누적 합계나 제너레이터 표현식에서 최종 값을 추출하는 사용 사례를 비교적 straightforward한 방식으로 작성할 수 있습니다.:

total = 0
partial_sums = [total := total + value for value in data]

factor = 1
while any(n % (factor := p) == 0 for p in small_primes):
    n //= factor

Guido도 이 일반적인 접근 방식에 대한 지지를 표명했습니다 [4].

이 PEP의 제안은 Tim의 원래 제안과 세 가지 주요 측면에서 다릅니다.

  • 이 제안은 단일 새로운 이름 바인딩 연산자에만 적용되는 것이 아니라 모든 증강 대입 연산자에 적용됩니다.
  • 실용적인 범위에서, 이름이 이미 정의되어 있어야 한다는 증강 대입의 요구 사항을 새로운 이름 바인딩 연산자에도 확장합니다(함수 스코프에서 새로운 지역 변수를 암시적으로 선언하는 대신 TargetNameError를 발생시킵니다).
  • 대상 이름 바인딩 목적상 무시되는 스코프 집합에 람다 표현식을 포함하여, 이러한 대입에 대한 투명성을 컴프리헨션과 제너레이터 표현식에만 한정하지 않고 모든 스코프 표현에 공통으로 적용합니다.

바인딩 대상을 계산할 때 스코프 표현을 무시하면, 제너레이터 표현식과 컴프리헨션에서 가장 바깥쪽 이터러블 표현식 간의 스코프 차이를 다시 감지하기가 어려워집니다(이를 감지하려면 클래스 스코프를 다루거나 반복 변수의 재바인딩을 시도해야 합니다). 따라서 이를 조정할 필요도 없습니다.

인라인 대입을 증강 대입의 변형으로 취급하기

관련 PEP 572의 과제 중 하나는 모든 스코프에서 NAME = EXPRNAME := EXPR이 의미론적으로 완전히 동등하다는 사실입니다. 이 때문에 문장 수준에서 어느 한쪽을 선택하도록 유도하는 본질적인 단서가 없으므로 두 형식을 가르치기가 어렵습니다. 결국 “NAME = EXPR가 더 오래 존재했기 때문에 선호됩니다”라고 설명해야 하며(그리고 PEP 572는 이러한 역사적 특이성을 컴파일러 수준에서 강제할 것을 제안합니다).

모듈 및 클래스 스코프에서 if NAME := EXPR:while NAME := EXPR:가 합리적으로 작동하도록 하면서 이러한 의미적 동등성을 피하기는 어렵습니다. 그러나 함수 스코프에서는 컴파일러가 모든 지역 이름을 포괄적으로 파악하므로, 사용 전에 이름을 대입하거나 선언하도록 요구할 수 있습니다. 이는 가능한 경우 계속해서 NAME = EXPR 형식을 기본값으로 사용하도록 하는 합리적인 동기를 제공하는 동시에, NAME := EXPR를 일종의 간단한 컴파일 시간 단언으로 사용할 수 있게 합니다(즉, 대상 이름이 이미 바인딩되었거나 선언되었으므로 컴파일러가 이미 알고 있어야 한다는 점을 명시적으로 나타냅니다).

Guido가 인라인 선언 지원을 엄격한 설계 요구 사항이라고 선언한다면, 이 PEP는 임의의 표현식 뒤에서 인라인 이름 선언을 지원하는 방법으로 EXPR given NAME도 도입하자는 제안으로 갱신될 것입니다(이를 통해 인라인 이름 선언을 복잡한 표현식의 중간에 삽입할 필요 없이 해당 표현식의 끝까지 미룰 수 있으며, PEP 8은 이러한 스타일을 권장하는 지침을 추가하게 됩니다).

클래스 수준 스코프 표현식에서 증강 대입 금지하기

현대적인 클래스는 메서드 구현에서 볼 수 있는 암시적 클로저를 정의하여 영인자 super() 호출에서 __class__를 사용할 수 있게 하지만, 사용자 수준 코드가 해당 스코프에 추가 이름을 명시적으로 넣을 방법은 없습니다.

한편 클래스 본문에 정의된 속성은 메서드의 어휘적 클로저를 정의할 때 무시되므로, 그곳에 속성을 추가하는 방식은 구현 수준에서도 작동하지 않습니다.

이러한 본질적인 모호성을 해결하려 하기보다, 이 PEP는 이러한 사용을 단순히 금지하고 영향을 받는 로직을 클래스 본문에 직접 인라인하지 않은 다른 곳에 작성하도록 요구합니다(예를 들어 별도의 도우미 함수에서 작성합니다).

비교 연산자와 대입 연산자

표현식으로 사용되는 OP= 구성은 현재 비교 연산을 나타냅니다.:

x == y # Equals
x >= y # Greater-than-or-equal-to
x <= y # Less-than-or-equal-to

이 PEP와 PEP 572는 모두 외관상 다소 유사하지만 실제로는 대입을 정의하는 연산자를 하나 이상 추가할 것을 제안합니다.:

x := y # Becomes

이 PEP는 여기서 더 나아가 모든 13개의 증강 대입 기호를 이항 연산자로 사용할 수 있도록 허용합니다.:

x +=  y # In-place add
x -=  y # In-place minus
x *=  y # In-place multiply
x @=  y # In-place matrix multiply
x /=  y # In-place division
x //= y # In-place int division
x %=  y # In-place mod
x &=  y # In-place bitwise and
x |=  y # In-place bitwise or
x ^=  y # In-place bitwise xor
x <<= y # In-place left shift
x >>= y # In-place right shift
x **= y # In-place power

이러한 추가 이항 연산자 중 가장 문제가 될 가능성이 큰 것은 비트 시프트 대입 연산자입니다. 각각이 포괄적 순서 비교 연산자 중 하나와 문자가 하나만 겹쳐 보이기 때문입니다.

예제

재시도 루프 단순화하기

현재 재시도 루프를 작성하는 몇 가지 방법이 있으며, 다음을 포함합니다.:

# Post-decrementing a counter
remaining_attempts = MAX_ATTEMPTS
while remaining_attempts:
    remaining_attempts -= 1
    try:
        result = attempt_operation()
    except Exception as exc:
        continue # Failed, so try again
    log.debug(f"Succeeded after {attempts} attempts")
    break # Success!
else:
    raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc

# Loop-and-a-half with a pre-incremented counter
attempt = 0
while True:
    attempts += 1
    if attempts > MAX_ATTEMPTS:
        raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
    try:
        result = attempt_operation()
    except Exception as exc:
        continue # Failed, so try again
    log.debug(f"Succeeded after {attempts} attempts")
    break # Success!

사용 가능한 각 방법은 상태 변경, 종료 조건 또는 둘 다와 같이 의도된 루프 구조의 일부를 루프 본문 안에 숨깁니다.

이 PEP의 제안은 상태 변경과 종료 조건을 모두 루프 헤더에 직접 포함할 수 있도록 합니다.:

attempt = 0
while (attempt += 1) <= MAX_ATTEMPTS:
    try:
        result = attempt_operation()
    except Exception as exc:
        continue # Failed, so try again
    log.debug(f"Succeeded after {attempts} attempts")
    break # Success!
else:
    raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc

if-elif 연쇄 단순화하기

검사한 조건을 다시 바인딩해야 하는 if-elif 연쇄는 현재 중첩된 if-else 문을 사용하여 작성해야 합니다.:

m = pattern.match(data)
if m:
    ...
else:
    m = other_pattern.match(data)
    if m:
        ...
    else:
        m = yet_another_pattern.match(data)
        if m:
            ...
        else:
            ...

관련 PEP 572와 마찬가지로 이 PEP은 해당 연쇄의 else/if 부분을 축약할 수 있도록 하여, 일관되고 상호 배타적인 구조를 더 쉽게 파악할 수 있게 합니다.:

m = pattern.match(data)
if m:
    ...
elif m := other_pattern.match(data):
    ...
elif m := yet_another_pattern.match(data):
    ...
else:
    ...

관련 PEP 572와 달리 이 PEP은 대입 대상을 := 대상으로 처음 사용하기 전에, 값을 바인딩하거나(위에 표시된 것처럼) 적절한 명시적 타입 선언을 포함하여 해당 대상이 지역 변수임을 명시적으로 나타내도록 요구합니다.:

m: typing.re.Match
if m := pattern.match(data):
    ...
elif m := other_pattern.match(data):
    ...
elif m := yet_another_pattern.match(data):
    ...
else:
    ...

컴프리헨션에서 중간 값 캡처하기

이 PEP의 제안은 중간 값을 포함하는 블록 스코프로 내보냄으로써 컴프리헨션과 제너레이터 표현식에서 중간 값을 간단히 캡처하고 재사용할 수 있도록 합니다.:

factor: int
while any(n % (factor := p) == 0 for p in small_primes):
    n //= factor

total = 0
partial_sums = [total += value for value in data]

람다 표현식이 재사용 가능한 코드 썽크처럼 동작하도록 허용하기

이 PEP는 고전적인 클로저 사용 예제를 허용합니다.:

def make_counter(start=0):
    x = start
    def counter(step=1):
        nonlocal x
        x += step
        return x
    return counter

다음과 같이 축약합니다.:

def make_counter(start=0):
    x = start
    return lambda step=1: x += step

후자의 형식은 여전히 개념적으로 밀도 높은 코드이지만, (“def”, “nonlocal”, “return” 키워드와 “x” 변수 이름의 추가 반복 두 번이 “lambda” 키워드로 대체되어) 보일러플레이트가 줄어들기 때문에 실제로는 더 읽기 쉬워질 수 있다고 합리적으로 주장할 수 있습니다.

PEP 572와의 관계

인라인 할당을 허용해야 한다는 근거는 PEP 572에 제시되어 있습니다. 이 경쟁 PEP는 처음에는 PEP 572의 표현식 의미를 유지하면서 대체 표면 문법(EXPR given NAME = EXPR)을 제안하려 했지만, 임베디드 할당을 허용해야 하는 초기 동기 부여 사례 중 하나인 컴프리헨션과 제너레이터 표현식에서 누적 합계를 쉽게 계산할 수 있도록 하는 방안을 논의하면서 변경되었습니다.

그 결과 이 PEP는 PEP 572와 달리 인라인 증강 할당의 사용 사례에 주로 초점을 맞춥니다. 또한 현재 함수 호출 시점에 필연적으로 UnboundLocalError를 발생시키는 사례를 새로운 컴파일 시간 TargetNameError로 보고하도록 변환하는 효과도 있습니다.

그런 다음 이름 재바인딩 표현식(NAME := TARGET)을 위한 새로운 문법을 추가합니다. 이는 PEP 572에서 식별된 동일한 사용 사례를 처리하기 위해서일 뿐만 아니라, 새로운 증강 할당 의미를 설명하고 구현하며 해명하는 데 도움을 주는 저수준 기본 요소로 추가하는 것이며, 제안되는 유일한 변경 사항으로 추가하는 것은 아닙니다.

이 PEP의 작성자는 이 접근 방식이 이름 재바인딩의 새로운 유연성이 지닌 가치를 더 명확하게 보여 주는 동시에, NAME = EXPR보다 NAME := EXPR을 언제 사용할지(또는 그 반대)를 설명하는 것과 관련하여 PEP 572에서 제기된 많은 잠재적 우려를 완화한다고 생각합니다. 또한 NAME := EXPR의 단독 문장 형식을 전면적으로 금지하지 않고도 이를 달성합니다. 즉, NAME := EXPR은 컴파일 오류가 되지만 (NAME := EXPR)은 허용됩니다.

감사의 말

PEP 작성자는 PEP 572에 대한 작업과, python-ideas 및 python-dev 양쪽에서 발생한 방대하고 산발적인 논의들을 일관된 형태로 요약하려 노력해 준 Chris Angelico에게 감사를 표하고자 합니다. 또한 스코프가 지정된 표현식 내부의 증강 대입에 대해 위와 같은 스코프 제안을 이끌어낸 부모 지역 스코프에 관한 심도 있는 논의를 해 준 Tim Peters에게도 감사를 표합니다.

이 PEP의 사전 공개 버전에 대한 Eric Snow의 피드백은 가독성을 크게 향상시키는 데 도움이 되었습니다.

참고 자료