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
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 함수 또는 클래스 정의의 이름 바인딩 단계를 재정의할 수 있도록 하는 새로운 @in 데코레이터 절의 추가를 제안합니다.
새로운 절은 장식된 함수 또는 클래스 정의를 순방향 참조할 수 있는 하나의 단순 문장을 허용합니다.
이 새로운 절은 “일회성” 함수 또는 클래스가 필요하고, 이를 사용하는 문장보다 함수 또는 클래스 정의를 앞에 배치하면 오히려 코드를 읽기 어렵게 만드는 경우에 사용하도록 설계되었습니다. 또한 새로운 이름이 @in 절의 문장에서만 보이도록 하여 이름 가림 문제도 방지합니다.
이 PEP는 PEP 3150(문장 지역 네임스페이스)의 여러 아이디어를 크게 기반으로 하므로, 근거의 일부 요소는 해당 PEP의 독자에게 익숙할 것입니다. 두 PEP 모두 현재로서는 보류된 상태이며, 이는 주로 어느 PEP에도 설득력 있는 실제 사용 사례가 부족하기 때문입니다.
기본 예제
이 문제의 오랜 역사와 이 구체적인 제안의 해법에 대한 자세한 근거를 살펴보기에 앞서, 이 제안이 단순화하도록 설계된 코드 유형의 간단한 예제를 몇 가지 살펴보겠습니다.
간단한 예로, 약한 참조 콜백은 다음과 같이 정의할 수 있습니다.:
@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 = <defined function or class>” 이름 바인딩 연산을 재정의할 수 있도록 하는 것이며, 특히 지역 이름 바인딩이 실제로 필요하지 않은 경우를 대상으로 합니다.
이 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
설계 논의
배경
“여러 줄 람다”에 관한 문제는 아주 오랫동안 많은 Python 사용자에게 골칫거리였으며, Ruby의 블록 기능을 살펴본 뒤에야 이것이 사람들을 그토록 괴롭히는 이유를 마침내 이해할 수 있었습니다. 즉, Python에서는 해당 함수를 필요로 하는 연산보다 먼저 함수에 이름을 붙이고 정의해야 하므로 개발자의 사고 흐름이 끊깁니다. 사용자는 “<X>를 수행하는 일회성 연산이 필요합니다”라는 지점에 이르게 되지만, 그것을 곧바로 말하기만 할 수 있는 대신 뒤로 돌아가 <X>를 수행할 함수를 이름 붙여 정의한 다음, 처음부터 실제로 수행하려던 연산에서 그 함수를 호출해야 합니다. 람다 표현식이 때때로 도움이 될 수 있지만, 완전한 기능 모음을 사용할 수 있는 것을 대신할 수는 없습니다.
Ruby의 블록 구문은 문마다 하나의 익명 함수로 제한되더라도 익명 함수가 여전히 매우 유용할 수 있음을 분명히 보여 주었으며, 이 PEP의 해결책이 취한 스타일에도 큰 영감을 주었습니다. Python에 하나의 표현식이 대부분의 복잡한 작업을 담당하는 구문이 얼마나 많은지 생각해 보십시오.
- 컴프리헨션, 제너레이터 표현식, map(), filter()
- sorted(), min(), max()에 전달하는 키 인자
- 부분 함수 적용
- 콜백 제공(예: 약한 참조 또는 비동기 IO를 위한 콜백)
- NumPy의 배열 브로드캐스트 연산
그러나 Ruby의 블록 구문을 그대로 도입하는 방식은 Python에서 작동하지 않습니다. Ruby 블록의 효과는 함수가 정의되는 방식의 다양한 관례에 크게 의존하기 때문입니다. 특히 블록을 직접 호출할 때 Ruby의 yield 구문을 사용하고, 함수의 마지막 인자로 블록을 받기 위해 &arg 메커니즘을 사용합니다.
Python은 오랫동안 이름이 지정된 함수에 의존해 왔으므로 콜백을 받는 API의 시그니처가 훨씬 다양하며, 따라서 일회성 함수를 적절한 위치에 끼워 넣을 수 있는 해결책이 필요합니다.
이 PEP에서 취한 접근 방식은 함수에 명시적으로 이름을 붙여야 한다는 요구 사항을 유지하되, 개발자의 사고 흐름에 맞도록 함수 정의와 해당 함수를 참조하는 문의 상대적인 순서를 변경할 수 있도록 하는 것입니다. 그 근거는 본질적으로 데코레이터를 도입할 때 사용한 것과 동일하지만, 더 광범위한 응용 사례를 포괄합니다.
PEP 3150과의 관계
PEP 3150(문 지역 네임스페이스)은 일반 대입문을 class 및 def 문과 동등한 수준으로 끌어올리는 것을 주요 동기로 설명합니다. 즉, 정의할 항목의 값이 계산되는 방식에 관한 세부 정보보다 먼저 해당 항목의 이름을 독자에게 제시하는 것입니다. 이 PEP는 표준 함수 정의의 단순한 이름 바인딩을 다른 것으로 대체할 수 있도록 함으로써(예를 들어 함수의 결과를 값에 할당하는 방식) 다른 방법으로 동일한 목표를 달성합니다.
두 PEP는 동일한 작성자가 작성했음에도 서로 직접 경쟁합니다. PEP 403은 현재 상태에서 최소한의 변경만으로 유용한 기능을 달성하려는 미니멀리스트 접근 방식을 나타냅니다. 반면 이 PEP는 더 유연한 독립형 문 설계를 목표로 하며, 이를 위해 언어를 더 크게 변경해야 합니다.
참고로, 제너레이터 표현식의 동작을 정확히 설명하는 데에는 PEP 403이 더 적합하지만, 이 PEP는 데코레이터 절의 동작을 일반적으로 설명하는 데 더 적합합니다. 두 PEP 모두 컨테이너 컴프리헨션의 의미론을 적절히 설명할 수 있습니다.
키워드 선택
이 제안은 기존 구문과의 구문 분석 모호성 및 하위 호환성 문제를 피하기 위해 어떤 종류의 접두사를 반드시 필요로 합니다. 또한 뒤따르는 함수 또는 클래스 정의가 실행된 후에만 다음 코드가 실행된다는 점을 선언하므로, 독자에게도 이를 명확히 강조해야 합니다.
in 키워드는 전방 참조라는 개념을 나타내는 데 사용할 수 있는 기존 키워드로 선택되었습니다.
@ 접두사는 Python 프로그래머가 이미 데코레이터 구문을 순서가 뒤바뀐 실행의 표시로 익숙하게 사용한다는 점을 활용하기 위해 포함되었습니다. 이 경우 함수 또는 클래스가 먼저 정의된 다음 데코레이터가 역순으로 적용됩니다.
함수의 경우 이 구문은 “NAME을 참조하는 <이 문> 안에서, <operation>을 수행하는 함수로 NAME을 정의합니다”라고 읽도록 의도되었습니다.
영어 산문으로의 매핑은 클래스 정의의 경우 그다지 명확하지 않지만, 개념은 동일합니다.
짧은 이름을 사용하는 함수와 클래스를 위한 향상된 디버깅 지원
람다 표현식을 널리 사용하는 것에 대한 반론 중 하나는 람다 표현식이 트레이스백의 이해 가능성과 자기 검사의 다른 측면에 부정적인 영향을 미친다는 것입니다. 짧고 난해한 함수 이름의 사용을 조장하는 구문에 대해서도 비슷한 반론이 제기됩니다(이 구문도 여기에 포함되며, 뒤따르는 정의의 이름을 최소 두 번 제공해야 하므로 f와 같은 축약형 자리 표시자 이름의 사용을 조장합니다).
그러나 PEP 3155에서 한정된 이름이 도입됨에 따라, 익명 클래스와 함수도 서로 다른 스코프에 존재하면 이제 서로 다른 표현을 갖게 됩니다. 예를 들어 다음과 같습니다.:
>>> def f():
... return lambda: y
...
>>> f()
<function f.<locals>.<lambda> at 0x7f6f46faeae0>
same 범위 내의 익명 함수(또는 이름을 공유하는 함수)는 객체 ID를 제외하면 여전히 동일한 표현을 공유하지만, 이는 객체 ID를 except 모든 것이 동일했던 과거의 상황에 비하면 여전히 크게 개선된 것입니다.
가능한 구현 전략
이 제안은 PEP 3150보다 적어도 한 가지 엄청난 장점을 갖습니다. 구현이 비교적 간단할 것입니다.
@in 절은 관련 함수 또는 클래스 정의와 이를 참조하는 문장의 AST에 포함됩니다. @in 절이 있으면 함수 또는 클래스 정의가 일반적으로 암시하는 로컬 이름 바인딩 연산 대신 해당 절이 방출됩니다.
잠재적으로 까다로운 유일한 부분은 in 문장의 스코프 안에서 문장 로컬 함수 또는 네임스페이스에 대한 참조의 의미를 변경하는 것이지만, 컴파일러 내부에서 추가 상태를 유지하면 어렵지 않게 처리할 수 있을 것입니다(전체 중첩 스위트에서 알 수 없는 수의 이름을 처리하는 것보다 단일 이름에 대해 처리하는 것이 훨씬 쉽습니다).
컨테이너 컴프리헨션과 제너레이터 표현식 설명
제안된 구문의 흥미로운 특징 중 하나는 제너레이터 표현식과 컨테이너 컴프리헨션 모두의 스코프 지정 및 실행 순서 의미를 설명하는 기본 요소로 사용할 수 있다는 점입니다.:
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의 데코레이터에서 영감을 받은 구문은 가독성을 해치는 대신 실제로 가독성을 향상해야 하는 경우로 새 기능을 명시적으로 제한합니다. 데코레이터를 처음 도입했을 때와 마찬가지로, 이 새로운 구문의 취지는 이를 can 사용할 수 있다면(즉, 함수의 지역 이름 바인딩이 완전히 불필요하다면) 아마 should 사용해야 한다는 것입니다.
이 아이디어의 또 다른 가능한 변형은 이 PEP의 데코레이터 기반 의미론을 유지하면서 PEP 3150의 더 보기 좋은 구문을 채택하는 것입니다.:
x = weakref.ref(target, report_destruction) given:
def report_destruction(obj):
print("{} is being destroyed".format(obj))
이 접근 방식에는 몇 가지 문제가 있습니다. 주요 문제는 이 구문 변형이 스위트처럼 보이지만 실제로는 스위트가 아닌 것을 사용한다는 점입니다. 부차적인 우려는 선행 표현식의 어느 이름(들)이 순방향 참조인지 컴파일러가 어떻게 알 수 있는지가 명확하지 않다는 점입니다(다만 언어 문법에서 suite-that-is-not-a-suite를 적절히 정의하면 이를 해결할 가능성이 있습니다).
그러나 중첩 스위트가 완전히 배제된 것은 아직 아닙니다. 관련 PEP 3150의 최신 버전은 구문의 의미론을 크게 단순화하는 명시적 전방 참조 및 이름 바인딩 방식을 사용하며, 단일 함수나 클래스 정의로 제한되지 않고 임의의 하위 표현식을 정의할 수 있다는 장점도 제공합니다.
참고 자료
Copyright
This document has been placed in the public domain.