[Deferred] PEP 403 - General purpose decorator clause (aka “@in” clause)

원문 링크: PEP 403 - General purpose decorator clause (aka “@in” clause)

상태: Deferred 유형: Standards Track 작성일: 13-Oct-2011

PEP 403 – 범용 데코레이터 절 (일명 “@in” 절)

개요 (Abstract)

이 PEP는 함수 또는 클래스 정의의 이름 바인딩(name binding) 단계를 오버라이드(override)할 수 있게 해주는 새로운 @in 데코레이터 절의 추가를 제안합니다. 이 새로운 절은 데코레이트될 함수 또는 클래스 정의에 대한 전방 참조(forward reference)를 만들 수 있는 단일 심플 스테이트먼트(simple statement)를 허용합니다.

@in 절은 “일회성(one-shot)” 함수 또는 클래스가 필요할 때 사용하도록 설계되었으며, 함수나 클래스 정의를 사용하는 스테이트먼트보다 먼저 배치하는 것이 코드를 읽기 더 어렵게 만드는 경우에 유용합니다. 또한 @in 절 내의 스테이트먼트에만 새 이름이 보이도록 하여 이름 쉐도잉(name shadowing) 문제를 방지합니다.

이 PEP는 PEP 3150 (Statement Local Namespaces)의 많은 아이디어를 기반으로 하고 있어, 해당 PEP를 읽은 독자에게는 일부 근거가 익숙할 것입니다. 두 PEP 모두 현재로서는 실제 사용 사례의 부족으로 인해 보류(deferred) 상태입니다.

기본 예시 (Basic Examples)

문제의 긴 역사와 제안된 해결책에 대한 자세한 근거를 설명하기 전에, 이 제안이 단순화하려는 코드 유형의 몇 가지 간단한 예시를 살펴보겠습니다.

약한 참조(weakref) 콜백 예시:

@in x = weakref.ref(target, report_destruction)
def report_destruction(obj):
    print("{} is being destroyed".format(obj))

이는 현재의 (개념적으로) “순서가 뒤바뀐(out of order)” 구문과 대조됩니다:

def report_destruction(obj):
    print("{} is being destroyed".format(obj))
x = weakref.ref(target, report_destruction)

호출 가능한(callable) 객체를 여러 번 사용할 때는 이 구조가 괜찮지만, 일회성(one-off) 작업에 강제되는 것은 불편합니다. 이름 반복이 특히 성가시다면 f와 같은 임시 이름(throwaway name)을 사용할 수 있습니다:

@in x = weakref.ref(target, f)
def f(obj):
    print("{} is being destroyed".format(obj))

정렬(sorted) 작업 예시: 마찬가지로, 특히 불완전하게 정의된 타입에 대한 sorted 작업은 다음과 같이 정의될 수 있습니다:

@in sorted_list = sorted(original, key=f)
def f(item):
    try:
        return item.calc_sort_order()
    except NotSortableError:
        return float('inf')

현재의 방식 대신:

def force_sort(item):
    try:
        return item.calc_sort_order()
    except NotSortableError:
        return float('inf')
sorted_list = sorted(original, key=force_sort)

List Comprehension의 조기 바인딩(early binding) 예시: List Comprehension에서 조기 바인딩 시맨틱(semantics)은 다음과 같이 얻을 수 있습니다:

@in funcs = [adder(i) for i in range(10)]
def adder(i):
    return lambda x: x + i

제안 (Proposal)

이 PEP는 기존의 클래스 및 함수 데코레이터 문법의 변형인 새로운 @in 절의 추가를 제안합니다. 새로운 @in 절은 데코레이터 라인 앞에 오며, 뒤따르는 함수 또는 클래스 정의에 대한 전방 참조를 허용합니다. 뒤따르는 함수 또는 클래스 정의는 항상 이름이 지정되며, 이 이름은 @in 절에서 전방 참조를 만드는 데 사용됩니다.

@in 절은 어떤 심플 스테이트먼트(simple statement)라도 포함할 수 있습니다 (예: pass와 같이 그 컨텍스트에서 의미 없는 스테이트먼트도 포함 가능). 이러한 허용적인 구조는 정의하고 설명하기 더 쉽지만, “의미 있는” 작업만 허용하는 더 제한적인 접근 방식도 가능합니다 (가능한 후보 목록은 PEP 3150 참조).

@in 절은 새로운 스코프(scope)를 생성하지 않습니다. 뒤따르는 함수 또는 클래스 정의를 제외한 모든 이름 바인딩 작업은 포함하는 스코프에 영향을 미칩니다. 뒤따르는 함수 또는 클래스 정의에 사용된 이름은 관련 @in 절에서만 보이며, 마치 해당 스코프에 정의된 일반 변수처럼 동작합니다. @in 절이나 뒤따르는 함수 또는 클래스 정의 내에 중첩된 스코프가 생성되면, 해당 스코프는 포함하는 스코프 내의 해당 이름에 대한 다른 바인딩이 아닌, 뒤따르는 함수 또는 클래스 정의를 보게 됩니다.

이 제안은 모든 함수 또는 클래스 정의의 일부인 암묵적인 “name = <정의된 함수="" 또는="" 클래스="">" 이름 바인딩 작업을 오버라이드할 수 있도록 하는 것이며, 특히 지역 이름 바인딩이 실제로 필요하지 않은 경우에 해당합니다.

이 PEP에 따르면, 일반적인 클래스 또는 함수 정의:

@deco2
@deco1
def name():
    ...

은 대략 다음과 동일하게 설명될 수 있습니다:

@in name = deco2(deco1(name))
def name():
    ...

문법 변경 (Syntax Change)

문법적으로는 단 하나의 새로운 문법 규칙만 필요합니다:

in_stmt: '@in' simple_stmt decorated

(전체 문법은 http://hg.python.org/cpython/file/default/Grammar/Grammar 참조)

설계 논의 (Design Discussion)

배경 (Background)

“다중 라인 람다(multi-line lambdas)”에 대한 질문은 오랫동안 많은 Python 사용자들을 괴롭혀 왔습니다. Ruby의 블록(block) 기능을 탐색하면서 비로소 이 문제가 사람들을 왜 그렇게 괴롭히는지 이해하게 되었습니다. Python은 함수가 필요로 하는 작업 전에 함수를 명명하고 도입해야 한다는 요구 사항 때문에 개발자의 사고 흐름을 방해합니다. 개발자들은 “를 수행하는 일회성 작업이 필요하다"는 지점에 도달했을 때, 직접적으로 표현할 수 있는 대신, 뒤로 돌아가서 를 수행할 함수를 명명한 다음, 처음부터 수행하려던 작업에서 해당 함수를 호출해야 합니다. 람다 표현식(Lambda expressions)이 때로는 도움이 되지만, 완전한 스위트(suite)를 사용할 수 있는 것을 대체할 수는 없습니다.

Ruby의 블록 문법은 또한 이 PEP의 해결책 스타일에 큰 영감을 주었습니다. 이는 스테이트먼트당 하나의 익명 함수로 제한되더라도 익명 함수가 여전히 엄청나게 유용할 수 있음을 명확히 보여주었습니다. Python이 많은 “무거운 작업(heavy lifting)”을 하나의 표현식이 담당하는 구문(constructs)을 가지고 있음을 고려하십시오:

  • Comprehensions (컴프리헨션), Generator Expressions (제너레이터 표현식)
  • map(), filter()
  • sorted(), min(), max()key 인자
  • Partial Function Application (부분 함수 적용)
  • 콜백(callback) 제공 (예: 약한 참조 또는 비동기 I/O)
  • NumPy의 Array Broadcast Operations (배열 브로드캐스트 작업)

그러나 Ruby의 블록 문법을 직접 채택하는 것은 Python에 적합하지 않습니다. Ruby 블록의 효율성은 함수 정의 방식의 다양한 규칙(특히, Ruby의 yield 문법을 사용하여 블록을 직접 호출하고 &arg 메커니즘을 사용하여 블록을 함수의 마지막 인자로 받는 것)에 크게 의존하기 때문입니다. Python은 오랫동안 명명된 함수에 의존해왔기 때문에 콜백을 받아들이는 API의 시그니처(signatures)는 훨씬 더 다양합니다. 따라서 일회성 함수가 적절한 위치에 삽입될 수 있는 해결책이 필요합니다.

이 PEP에서 취하는 접근 방식은 함수를 명시적으로 명명해야 한다는 요구 사항을 유지하면서, 정의와 이를 참조하는 스테이트먼트의 상대적인 순서를 개발자의 사고 흐름에 맞게 변경할 수 있도록 하는 것입니다. 이 근거는 본질적으로 데코레이터를 도입할 때 사용된 것과 동일하지만, 더 넓은 범위의 애플리케이션을 포괄합니다.

PEP 3150과의 관계 (Relation to PEP 3150)

PEP 3150 (Statement Local Namespaces)은 정의될 항목의 이름이 해당 값의 계산 세부 정보보다 먼저 독자에게 제시되는 클래스 및 def 스테이트먼트와 동일하게 일반 할당 스테이트먼트(assignment statements)를 격상시키는 것을 주된 동기로 설명합니다. 이 PEP는 표준 함수 정의의 간단한 이름 바인딩을 다른 것(예: 함수의 결과를 값에 할당하는 것)으로 대체할 수 있도록 하여 동일한 목표를 다른 방식으로 달성합니다.

두 PEP 모두 동일한 저자가 작성했음에도 불구하고 서로 직접적인 경쟁 관계에 있습니다. PEP 403은 현 상태에서 최소한의 변경으로 유용한 기능을 달성하려는 미니멀리스트(minimalist) 접근 방식을 나타냅니다. 이 PEP는 대신 더 유연한 독립형 스테이트먼트 설계를 목표로 하며, 이는 언어에 더 큰 정도의 변경을 요구합니다.

PEP 403이 제너레이터 표현식의 동작을 정확하게 설명하는 데 더 적합한 반면, 이 PEP는 데코레이터 절의 동작을 일반적으로 설명하는 데 더 적합합니다. 두 PEP 모두 컨테이너 컴프리헨션(container comprehensions)의 시맨틱에 대한 적절한 설명을 지원합니다.

키워드 선택 (Keyword Choice)

이 제안은 구문 분석 모호성(parsing ambiguity)과 기존 구문과의 역호환성(backwards compatibility) 문제를 피하기 위해 어떤 종류의 접두사(prefix)가 확실히 필요합니다. 또한, 뒤따르는 코드 조각이 뒤따르는 함수 또는 클래스 정의가 실행된 후에만 실행될 것임을 선언하므로 독자에게 명확하게 강조되어야 합니다.

in 키워드는 전방 참조의 개념을 나타내는 데 사용될 수 있는 기존 키워드로 선택되었습니다. @ 접두사는 Python 프로그래머들이 이미 데코레이터 문법을 순서가 뒤바뀐 실행(out of order execution)의 표시로 사용하고 있다는 사실을 활용하기 위해 포함되었습니다. 여기서 함수 또는 클래스는 실제로 먼저 정의된 다음 데코레이터가 역순으로 적용됩니다.

함수의 경우, 이 구문은 “in define NAME as a function that does "으로 읽히도록 의도되었습니다. 클래스 정의의 경우 영어 산문(prose)으로의 매핑이 그렇게 명확하지는 않지만, 개념은 동일합니다.

짧은 이름의 함수 및 클래스를 위한 더 나은 디버깅 지원 (Better Debugging Support for Functions and Classes with Short Names)

람다 표현식의 광범위한 사용에 대한 반대 의견 중 하나는 트레이스백(traceback) 가독성 및 기타 인트로스펙션(introspection) 측면에 부정적인 영향을 미친다는 것입니다. 짧고 모호한 함수 이름을 장려하는 구문(뒤따르는 정의의 이름이 적어도 두 번 제공되어야 하므로 f와 같은 약칭 플레이스홀더(placeholder) 이름의 사용을 장려하는 이 구문 포함)에 대해서도 비슷한 반대 의견이 제기됩니다.

그러나 PEP 3155에서 Qualified Names(정규화된 이름)의 도입은 익명 클래스와 함수라도 다른 스코프에서 발생하는 경우 이제 다른 표현을 갖게 된다는 것을 의미합니다. 예를 들어:

>>> def f():
...     return lambda: y
...
>>> f()
<function f.<locals>.<lambda> at 0x7f6f46faeae0>

동일한 스코프 내의 익명 함수 (또는 이름을 공유하는 함수)는 여전히 표현을 공유하지만 (객체 ID 제외), 이는 객체 ID를 제외한 모든 것이 동일했던 과거 상황에 비해 여전히 주요 개선 사항입니다.

가능한 구현 전략 (Possible Implementation Strategy)

이 제안은 PEP 3150보다 적어도 한 가지 거대한 이점이 있습니다. 구현이 비교적 간단해야 한다는 것입니다.

@in 절은 관련 함수 또는 클래스 정의 및 이를 참조하는 스테이트먼트에 대한 AST(Abstract Syntax Tree)에 포함될 것입니다. @in 절이 존재할 때, 일반적으로 함수 또는 클래스 정의에 의해 암묵적으로 발생하는 지역 이름 바인딩 작업 대신 발행될 것입니다.

잠재적으로 까다로운 한 가지 부분은 in 스테이트먼트의 스코프 내에서 스테이트먼트 로컬 함수 또는 네임스페이스에 대한 참조의 의미를 변경하는 것이지만, 컴파일러 내에 일부 추가 상태를 유지함으로써 해결하기 어렵지 않을 것입니다 (전체 중첩된 스위트에서 알 수 없는 수의 이름에 대해 처리하는 것보다 단일 이름에 대해 처리하는 것이 훨씬 쉽습니다).

컨테이너 컴프리헨션 및 제너레이터 표현식 설명 (Explaining Container Comprehensions and Generator Expressions)

제안된 구문의 흥미로운 특징 중 하나는 제너레이터 표현식과 컨테이너 컴프리헨션 모두의 스코핑(scoping) 및 실행 순서 시맨틱을 설명하는 원시적인(primitive) 방법으로 사용될 수 있다는 것입니다:

seq2 = [x for x in y if q(x) for y in seq if p(y)]
# 다음 등가
@in seq2 = f(seq):
    def f(seq)
        result = []
        for y in seq:
            if p(y):
                for x in y:
                    if q(x):
                        result.append(x)
        return result

이 확장(expansion)에서 중요한 점은 컴프리헨션이 클래스 스코프에서 오작동하는 것처럼 보이는 이유를 설명한다는 것입니다. 가장 바깥쪽 이터레이터(outermost iterator)만 클래스 스코프에서 평가되고, 모든 프레디케이트(predicates), 중첩된 이터레이터, 값 표현식은 중첩된 스코프 내에서 평가됩니다.

제너레이터 표현식에 대해서도 등가적인 확장이 가능합니다:

gen = (x for x in y if q(x) for y in seq if p(y))
# 다음 등가
@in gen = g(seq):
    def g(seq)
        for y in seq:
            if p(y):
                for x in y:
                    if q(x):
                        yield x

더 많은 예시 (More Examples)

지역 네임스페이스를 오염시키지 않고 속성 계산 (from os.py):

# 현재 Python (수동 네임스페이스 정리)
def _createenviron():
    ... # 27줄 함수
environ = _createenviron()
del _createenviron

# 다음으로 변경
@in environ = _createenviron()
def _createenviron():
    ... # 27줄 함수

루프 조기 바인딩 (Loop early binding):

# 현재 Python (기본 인자 트릭)
funcs = [(lambda x, i=i: x + i) for i in range(10)]

# 다음으로 변경
@in funcs = [adder(i) for i in range(10)]
def adder(i):
    return lambda x: x + i

# 또는 심지어:
@in funcs = [adder(i) for i in range(10)]
def adder(i):
    @in return incr
    def incr(x):
        return x + i

뒤따르는 클래스를 스테이트먼트 로컬 네임스페이스로 사용:

# 서브 표현식을 한 번만 평가
@in c = math.sqrt(x.a*x.a + x.b*x.b)
class x:
    a = calculate_a()
    b = calculate_b()

함수를 유효한 식별자가 아닌 위치에 직접 바인딩:

@in dispatch[MyClass] = f
def f():
    ...

데코레이터 남용에 가까운 구문을 제거할 수 있습니다:

# 현재 Python
@call
def f():
    ...

# 다음으로 변경
@in f()
def f():
    ...

참고 구현 (Reference Implementation)

아직 없습니다.

감사 (Acknowledgements)

Ruby의 블록에 대해 비판할 때 제가 무엇을 말하는지 전혀 몰랐음을 솔직하게 지적해 준 Gary Bernhardt에게 깊은 감사를 드립니다. 이는 매우 깨달음을 주는 조사 과정을 시작하는 계기가 되었습니다.

거부된 개념 (Rejected Concepts)

이 섹션에서는 이전에 다루었던 내용을 다시 반복하지 않기 위해 거부된 대안들을 문서화합니다.

데코레이터 접두사 문자 생략 (Omitting the decorator prefix character)

이 제안의 초기 버전에서는 @ 접두사를 생략했습니다. 그러나 이 접두사 없이는 맨몸의 in 키워드가 이후의 함수 또는 클래스 정의와 충분히 강력하게 연결되지 않았습니다. 데코레이터 접두사를 재사용하고 새로운 구문을 일종의 데코레이터 절로 명시적으로 특징화함으로써 사용자들이 두 개념을 연결하고 동일한 아이디어의 두 가지 변형으로 이해하도록 돕는 것을 목표로 합니다.

익명 전방 참조 (Anonymous Forward References)

이 PEP의 이전 버전 (참고)은 새로운 절이 :로 시작하고 전방 참조가 @를 사용하여 작성되는 문법을 제안했습니다. 이 변형에 대한 피드백은 거의 전적으로 부정적이었는데, 보기 흉하고 지나치게 마법 같다는 의견이 많았습니다:

:x = weakref.ref(target, @)
def report_destruction(obj):
    print("{} is being destroyed".format(obj))

더 최근의 변형은 항상 전방 참조에 ...를 사용했으며, 진정으로 익명인 함수 및 클래스 정의를 사용했습니다. 그러나 이는 더 복잡한 경우에 빠르게 이해하기 어려운 점들의 덩어리로 변질되었습니다:

in funcs = [...(i) for i in range(10)]
def ...(i):
    in return ...
    def ...(x):
        return x + i
in c = math.sqrt(....a*....a + ....b*....b)
class ...:
    a = calculate_a()
    b = calculate_b()

중첩된 스위트 사용 (Using a nested suite)

전체 중첩된 스위트(nested suite)를 사용하는 문제점은 PEP 3150에 가장 잘 설명되어 있습니다. 이를 제대로 구현하는 것은 비교적 어렵고, 스코핑 시맨틱은 설명하기 더 어려우며, 둘 중 하나를 선택하기 위한 명확한 지침 없이 두 가지 방식으로 수행할 수 있는 상황을 많이 만듭니다 (거의 모든 일반적인 명령형 코드로 표현될 수 있는 구문이 given 스테이트먼트로 표현될 수 있기 때문). PEP는 이 마지막 문제를 해결하기 위한 새로운 PEP 8 가이드라인을 제안하지만, 구현의 어려움은 그렇게 쉽게 다루어지지 않습니다.

대조적으로, 이 PEP의 데코레이터에서 영감을 받은 문법은 새로운 기능을 가독성을 해치는 대신 실제로 향상시켜야 하는 경우로 명시적으로 제한합니다. 데코레이터의 원래 도입 사례와 마찬가지로, 이 새로운 문법의 아이디어는 만약 그것이 사용될 수 있다면 (즉, 함수의 지역 이름 바인딩이 완전히 불필요하다면) 아마도 사용해야 한다는 것입니다.

이 아이디어의 또 다른 가능한 변형은 이 PEP의 데코레이터 기반 시맨틱을 유지하면서 PEP 3150의 더 예쁜 문법을 채택하는 것입니다:

x = weakref.ref(target, report_destruction)
given:
    def report_destruction(obj):
        print("{} is being destroyed".format(obj))

이 접근 방식에는 몇 가지 문제가 있습니다. 주요 문제는 이 문법 변형이 스위트처럼 보이는 것을 사용하지만 실제로는 스위트가 아니라는 것입니다. 부차적인 문제는 컴파일러가 선행 표현식에서 어떤 이름이 전방 참조인지 어떻게 알 수 있는지 명확하지 않다는 것입니다 (비록 언어 문법에서 “스위트가 아닌 스위트”의 적절한 정의를 통해 잠재적으로 해결될 수 있겠지만).

그러나 중첩된 스위트가 완전히 배제된 것은 아닙니다. PEP 3150의 최신 버전은 스테이트먼트의 시맨틱을 크게 단순화하는 명시적인 전방 참조 및 이름 바인딩 스키마를 사용하며, 단일 함수 또는 클래스 정의로 제한되지 않고 임의의 서브 표현식 정의를 허용하는 장점을 제공합니다.

참조 (References)

python-ideas 스레드의 시작: https://mail.python.org/pipermail/python-ideas/2011-October/012276.html

이 문서는 퍼블릭 도메인(public domain)으로 지정되었습니다.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments