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

Python 개선 제안 한국어 번역

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

Author:
Alyssa Coghlan <ncoghlan at gmail.com>
Status:
Deferred
Type:
Standards Track
Created:
13-Oct-2011
Python-Version:
3.4
Post-History:
13-Oct-2011

Table of Contents

번역·라이선스 안내

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

초록

이 PEP는 함수나 클래스 정의의 이름 바인딩 단계를 재정의할 수 있게 하는 새로운 @in 데코레이터 절을 추가할 것을 제안합니다.

새 절은 데코레이트된 함수나 클래스 정의를 전방 참조할 수 있는 단순문 하나를 받습니다.

이 새 절은 “일회성” 함수나 클래스가 필요하지만, 그 함수나 클래스 정의를 사용하는 문장 앞에 두면 오히려 코드를 읽기 어려워지는 경우에 쓰도록 설계되었습니다. 또한 새 이름이 @in 절 안의 문장에서만 보이도록 보장하므로 이름 가림(name shadowing) 문제도 생기지 않습니다.

이 PEP는 PEP 3150 (문장 지역 네임스페이스)의 여러 아이디어에 크게 기반하고 있으므로, 근거의 일부 요소는 그 PEP를 읽은 독자에게 익숙할 것입니다. 두 PEP 모두 현재로서는 보류 상태이며, 주된 이유는 어느 PEP에도 설득력 있는 실제 사용 사례가 부족하기 때문입니다.

기본 예제

이 문제의 긴 역사와 이 구체적인 해결안에 대한 자세한 근거를 살펴보기 전에, 이 제안이 단순화하려는 종류의 코드를 보여 주는 간단한 예제를 몇 가지 소개합니다.

간단한 예로, weakref 콜백을 다음과 같이 정의할 수 있습니다:

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

이는 이 작업을 위한 현재의 (개념적으로) “순서가 뒤바뀐” 문법과 대조됩니다:

def report_destruction(obj):
    print("{} is being destroyed".format(obj))

x = weakref.ref(target, report_destruction)

호출 가능 객체를 여러 번 사용할 때는 그런 구조도 괜찮지만, 일회성 작업에서까지 그 구조를 강요받는 것은 번거롭습니다.

이름을 반복하는 것이 특히 거슬린다면, 대신 f와 같은 일회용 이름을 사용할 수 있습니다:

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

마찬가지로, 특히 허술하게 정의된 타입에 대한 정렬 작업도 이제 다음과 같이 정의할 수 있습니다:

@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)

그리고 리스트 컴프리헨션에서의 조기 바인딩 의미론은 다음과 같이 얻을 수 있습니다:

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

제안

이 PEP는 기존 클래스 및 함수 데코레이터 문법의 변형인 새로운 @in 절을 추가할 것을 제안합니다.

새 @in 절은 데코레이터 줄보다 앞에 오며, 뒤따르는 함수나 클래스 정의에 대한 전방 참조를 허용합니다.

뒤따르는 함수나 클래스 정의에는 항상 이름이 있으며, @in 절에서는 그 이름을 사용해 전방 참조를 합니다.

@in 절에는 어떤 단순문이든 넣을 수 있습니다(pass와 같이 그 맥락에서 아무 의미가 없는 문장도 포함됩니다. 그런 코드도 허용되기는 하지만 작성할 이유는 전혀 없습니다). 이처럼 허용 범위가 넓은 구조는 정의하기도 쉽고 설명하기도 쉽지만, “의미가 있는” 연산만 허용하는 더 제한적인 접근 방식도 가능합니다(가능한 후보 목록은 PEP 3150을 참조하십시오).

@in 절은 새 스코프를 만들지 않습니다. 뒤따르는 함수나 클래스 정의를 제외한 모든 이름 바인딩 연산은 이를 둘러싼 스코프에 영향을 줍니다.

뒤따르는 함수나 클래스 정의에 사용된 이름은 연관된 @in 절에서만 보이며, 그 스코프에서 정의된 일반 변수처럼 동작합니다. @in 절이나 뒤따르는 함수 또는 클래스 정의 안에서 중첩 스코프가 만들어지면, 그 스코프들은 둘러싼 스코프에 있는 같은 이름의 다른 바인딩이 아니라 뒤따르는 함수나 클래스 정의를 보게 됩니다.

실질적인 의미에서 이 제안은 모든 함수나 클래스 정의에 포함된 암묵적인 “name = <정의된 함수나 클래스>” 이름 바인딩 연산을 재정의할 수 있게 하는 것이며, 특히 지역 이름 바인딩이 실제로 필요하지 않은 경우를 위한 것입니다.

이 PEP에서 일반적인 클래스나 함수 정의는:

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

대략 다음과 동등한 것으로 설명할 수 있습니다:

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

문법 변경

문법적으로는 새 문법 규칙 하나만 있으면 됩니다:

in_stmt: '@in' simple_stmt decorated

문법: http://hg.python.org/cpython/file/default/Grammar/Grammar

설계 논의

배경

“여러 줄 람다(multi-line lambdas)” 문제는 아주 오랫동안 많은 Python 사용자를 괴롭혀 온 문제였으며, 저는 Ruby의 블록 기능을 살펴보고 나서야 이것이 왜 사람들에게 그토록 거슬리는 버그처럼 느껴지는지 마침내 이해할 수 있었습니다. Python이 함수를 필요로 하는 연산보다 앞서 그 함수에 이름을 붙이고 먼저 도입하도록 요구하는 것이 개발자의 사고 흐름을 끊어 놓기 때문입니다. 개발자는 “<X>를 수행하는 일회성 연산이 필요한데”라고 생각하는 지점에 이르지만, 그것을 그대로 직접 말할 수 없고, 한 걸음 물러나 <X>를 수행하는 함수에 이름을 붙인 다음, 애초에 하려던 연산에서 그 함수를 호출해야 합니다. 람다 표현식이 도움이 될 때도 있지만, 완전한 스위트를 사용할 수 있는 것을 대신하지는 못합니다.

Ruby의 블록 문법은 이 PEP가 제시하는 해법의 스타일에도 큰 영감을 주었습니다. 문장마다 익명 함수를 하나로 제한하더라도 익명 함수가 여전히 매우 유용할 수 있다는 점을 분명히 보여 주었기 때문입니다. Python에 하나의 표현식이 힘든 작업의 대부분을 담당하는 구문이 얼마나 많은지 생각해 보십시오.

  • 컴프리헨션, 제너레이터 표현식, map(), filter()
  • sorted(), min(), max()의 key 인자
  • 부분 함수 적용
  • 콜백 제공(예: 약한 참조나 비동기 IO를 위한 콜백)
  • NumPy의 배열 브로드캐스트 연산

하지만 Ruby의 블록 문법을 Python에 그대로 도입할 수는 없습니다. Ruby 블록의 효용은 함수가 정의되는 방식에 관한 여러 관례에 크게 의존하기 때문입니다(구체적으로는 블록을 직접 호출하는 Ruby의 yield 문법과, 블록을 함수의 마지막 인자로 받는 &arg 메커니즘입니다).

Python은 오랫동안 이름 있는 함수에 의존해 왔기 때문에 콜백을 받는 API의 시그니처가 훨씬 다양합니다. 따라서 일회성 함수를 적절한 위치에 끼워 넣을 수 있는 해결책이 필요합니다.

이 PEP가 택한 접근 방식은 함수에 명시적으로 이름을 붙여야 한다는 요구 사항은 유지하되, 함수 정의와 그 함수를 참조하는 문의 상대적 순서를 개발자의 사고 흐름에 맞게 바꿀 수 있도록 허용하는 것입니다. 그 근거는 본질적으로 데코레이터를 도입할 때 사용한 것과 같지만, 더 넓은 범위의 용도를 다룹니다.

PEP 3150과의 관계

PEP 3150 (문 지역 네임스페이스)은 주된 동기를 일반 대입문을 class 및 def 문과 대등한 수준으로 끌어올리는 것이라고 설명합니다. class 및 def 문에서는 정의할 항목의 이름이 그 항목의 값을 계산하는 방식에 관한 세부 사항보다 먼저 독자에게 제시됩니다. 이 PEP는 표준 함수 정의의 단순한 이름 바인딩을 다른 것(예: 함수의 결과를 값에 대입하는 것)으로 대체할 수 있도록 함으로써, 같은 목표를 다른 방식으로 달성합니다.

두 PEP는 저자가 같지만 서로 직접 경쟁하는 관계입니다. PEP 403은 현 상태에서 최소한의 변경만으로 유용한 기능을 달성하려는 최소주의적 접근 방식을 대표합니다. 반면 이 PEP는 더 유연한 독립형 문 설계를 목표로 하며, 이를 위해서는 언어를 더 크게 변경해야 합니다.

PEP 403이 제너레이터 표현식의 동작을 올바르게 설명하는 데 더 적합한 반면, 이 PEP는 데코레이터 절의 동작을 전반적으로 더 잘 설명할 수 있다는 점에 유의하십시오. 두 PEP 모두 컨테이너 컴프리헨션의 의미론을 적절히 설명할 수 있습니다.

키워드 선택

이 제안에는 파싱 모호성과 기존 구문과의 하위 호환성 문제를 피하기 위해 어떤 종류든 접두사가 반드시 필요합니다. 또한 이 접두사는 뒤따르는 코드 조각이 후행 함수 또는 클래스 정의가 실행된 이후에야 실행된다는 것을 선언하므로, 독자에게 분명하게 드러나야 합니다.

in 키워드는 전방 참조라는 개념을 나타내는 데 사용할 수 있는 기존 키워드이기 때문에 선택되었습니다.

@ 접두사를 포함한 것은, Python 프로그래머가 이미 데코레이터 문법을 순서가 바뀐 실행의 표시로 받아들이는 데 익숙하다는 점을 활용하기 위해서입니다. 데코레이터 문법에서는 함수나 클래스가 실제로 먼저 정의되고, 그다음에 데코레이터가 역순으로 적용됩니다.

함수의 경우, 이 구문은 “<NAME을 참조하는 이 문>에서 NAME을 <연산>을 수행하는 함수로 정의”라고 읽히도록 의도되었습니다.

클래스 정의의 경우에는 영어 문장으로의 매핑이 그만큼 명확하지는 않지만, 개념은 동일합니다.

짧은 이름을 가진 함수와 클래스에 대한 디버깅 지원 개선

람다 표현식을 널리 사용하는 데 대한 반대 의견 중 하나는 람다 표현식이 트레이스백의 이해 용이성과 인트로스펙션의 다른 측면에 부정적인 영향을 준다는 것입니다. 짧고 난해한 함수 이름을 조장하는 구문에 대해서도 비슷한 반대 의견이 제기됩니다(이 제안도 여기에 포함됩니다. 이 제안은 뒤따르는 정의의 이름을 최소 두 번 제공하도록 요구하므로 f와 같은 축약된 자리 표시자 이름의 사용을 부추깁니다).

그러나 PEP 3155에서 정규화된 이름(qualified name)이 도입되었으므로, 이제는 익명 클래스와 함수라도 서로 다른 스코프에 있으면 서로 다른 표현을 갖게 됩니다. 예를 들면 다음과 같습니다.:

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

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

가능한 구현 전략

이 제안은 PEP 3150에 비해 적어도 한 가지 엄청난 장점이 있습니다. 구현이 비교적 간단할 것이라는 점입니다.

@in 절은 연관된 함수 또는 클래스 정의와 그것을 참조하는 문의 AST에 포함됩니다. @in 절이 있으면 함수나 클래스 정의가 일반적으로 암시하는 지역 이름 바인딩 연산 대신 이 절이 생성됩니다.

잠재적으로 까다로울 수 있는 유일한 부분은 in 문의 스코프 안에 있는 동안 문 지역(statement local) 함수나 네임스페이스에 대한 참조의 의미를 바꾸는 것이지만, 컴파일러 안에 몇 가지 추가 상태를 유지하면 그리 어렵지 않게 해결할 수 있을 것입니다(완전한 중첩 스위트에서 알 수 없는 개수의 이름을 처리하는 것보다 단일 이름을 처리하는 것이 훨씬 쉽습니다).

컨테이너 컴프리헨션과 제너레이터 표현식 설명하기

제안된 구문의 흥미로운 특징 중 하나는 제너레이터 표현식과 컨테이너 컴프리헨션 모두의 스코프 규칙 및 실행 순서 의미론을 설명하는 기본 요소로 사용할 수 있다는 점입니다.:

seq2 = [x for x in y if q(x) for y in seq if p(y)]

# would be equivalent to

@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

이 확장에서 중요한 점은 컴프리헨션이 클래스 스코프에서 잘못 동작하는 것처럼 보이는 이유를 설명해 준다는 것입니다. 가장 바깥쪽 이터레이터만 클래스 스코프에서 평가되고, 모든 조건식, 중첩된 이터레이터, 값 표현식은 중첩된 스코프 안에서 평가됩니다.

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

gen = (x for x in y if q(x) for y in seq if p(y))

# would be equivalent to

@in gen = g(seq):
def g(seq)
    for y in seq:
        if p(y):
            for x in y:
                if q(x):
                    yield x

추가 예제

지역 네임스페이스를 오염시키지 않고 속성 계산하기 (os.py에서 발췌):

# Current Python (manual namespace cleanup)
def _createenviron():
    ... # 27 line function

environ = _createenviron()
del _createenviron

# Becomes:
@in environ = _createenviron()
def _createenviron():
    ... # 27 line function

루프 조기 바인딩:

# Current Python (default argument hack)
funcs = [(lambda x, i=i: x + i) for i in range(10)]

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

# Or even:
@in funcs = [adder(i) for i in range(10)]
def adder(i):
    @in return incr
    def incr(x):
        return x + i

뒤따르는 클래스를 문 지역 네임스페이스로 사용할 수 있습니다:

# Evaluate subexpressions only once
@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():
    ...

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

# Current Python
@call
def f():
    ...

# Becomes:
@in f()
def f():
    ...

참조 구현

아직 없습니다.

감사의 글

제가 Ruby의 블록을 비판하면서 무슨 말을 하는지 전혀 모르고 있다는 점을 거침없이 지적해 주어, 꽤 깨달음을 주는 탐구 과정을 시작하게 해 준 Gary Bernhardt에게 깊이 감사드립니다.

거부된 개념

이미 다룬 내용을 되풀이하지 않도록 거부된 몇 가지 대안을 이 절에 기록합니다.

데코레이터 접두 문자 생략

이 제안의 이전 버전에서는 @ 접두사를 생략했습니다. 그러나 그 접두사가 없으면 단독으로 쓰인 in 키워드만으로는 이 절을 뒤따르는 함수 또는 클래스 정의와 충분히 강하게 연관시키지 못했습니다. 데코레이터 접두사를 재사용하고 새 구문을 일종의 데코레이터 절로 명시적으로 규정하는 것은 사용자가 두 개념을 연결하여 같은 아이디어의 두 가지 변형으로 볼 수 있도록 돕기 위한 것입니다.

익명 전방 참조

이 PEP의 이전 버전([1] 참조)은 새 절을 :로 시작하고 전방 참조를 @로 표기하는 문법을 제안했습니다. 이 변형은 보기 흉하고 지나치게 마법 같다는 평가를 받아 피드백이 거의 한결같이 부정적이었습니다:

: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()

중첩된 스위트 사용

완전한 중첩 스위트를 사용할 때의 문제점은 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의 최신 버전은 문의 의미론을 크게 단순화하는 명시적인 전방 참조 및 이름 바인딩 방식을 사용하며, 단일 함수나 클래스 정의로 제한되지 않고 임의의 하위 표현식을 정의할 수 있다는 장점도 분명히 제공합니다.

참고 문헌