PEP 3104 – 바깥 스코프의 이름에 대한 접근
- Author:
- Ka-Ping Yee <ping at zesty.ca>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 12-Oct-2006
- Python-Version:
- 3.0
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
중첩 스코프를 지원하는 대부분의 언어에서는 코드가 가장 가까운 바깥 스코프의 어떤 이름이든 참조하거나 다시 바인딩(할당)할 수 있습니다. 현재 Python 코드는 모든 바깥 스코프의 이름을 참조할 수 있지만, 이름을 다시 바인딩할 수 있는 스코프는 두 곳뿐입니다. 단순 할당을 사용하는 지역 스코프와 global 선언을 사용하는 모듈 전역 스코프입니다.
이 제한은 Python-Dev 메일링 리스트와 그 밖의 여러 곳에서 여러 차례 제기되었으며, 이 제한을 제거하는 방법에 대한 장기간의 논의와 많은 제안으로 이어졌습니다. 이 PEP는 제안된 다양한 대안을 각각에 대해 언급된 장점 및 단점과 함께 요약합니다.
근거
버전 2.1 이전에는 Python의 스코프 처리가 표준 C와 비슷했습니다. 파일 안에는 전역과 지역이라는 두 단계의 스코프만 있었습니다. C에서는 함수 정의를 중첩할 수 없다는 사실에 따른 자연스러운 결과입니다. 그러나 Python에서는 함수가 보통 최상위 수준에서 정의되더라도 함수 정의를 어디에서나 실행할 수 있습니다. 이로 인해 Python은 의미 체계 없이 중첩 스코프의 구문적 외관을 갖게 되었고, 일부 프로그래머에게는 놀라운 불일치가 발생했습니다. 예를 들어 최상위 수준에서 작동하던 재귀 함수가 다른 함수 안으로 이동하면 더 이상 작동하지 않게 됩니다. 재귀 함수 자체의 이름이 해당 함수 본문의 스코프에서 더 이상 보이지 않기 때문입니다. 이는 함수가 서로 다른 컨텍스트에 배치되더라도 일관되게 동작해야 한다는 직관에 어긋납니다. 다음은 예입니다.:
def enclosing_function():
def factorial(n):
if n < 2:
return 1
return n * factorial(n - 1) # fails with NameError
print factorial(5)
Python 2.1에서는 모든 바깥 스코프에 바인딩된 이름을 보이게 하여 정적 중첩 스코프에 더 가까워졌습니다(PEP 227 참조). 이 변경으로 위의 코드 예제가 예상대로 작동합니다. 그러나 이름에 대한 모든 할당은 해당 이름을 암묵적으로 지역 이름으로 선언하므로, global 선언이 이름을 전역으로 강제하는 경우를 제외하면 바깥 스코프의 이름을 다시 바인딩할 수 없습니다. 따라서 버튼을 클릭하여 증가하거나 감소시킬 수 있는 숫자를 표시하려는 다음 코드는, 어휘적 스코프에 익숙한 사람이 예상하는 방식으로 작동하지 않습니다.:
def make_scoreboard(frame, score=0):
label = Label(frame)
label.pack()
for i in [-10, -1, 1, 10]:
def increment(step=i):
score = score + step # fails with UnboundLocalError
label['text'] = score
button = Button(frame, text='%+d' % i, command=increment)
button.pack()
return label
Python 구문에는 increment에서 언급된 이름 score가 지역 변수 increment가 아니라 make_scoreboard에 바인딩된 변수 score를 가리킨다는 것을 나타낼 방법이 없습니다. Python 사용자와 개발자는 이러한 제한을 제거하여 JavaScript, Perl, Ruby, Scheme, Smalltalk, GNU 확장을 적용한 C, C# 2.0을 비롯한 여러 프로그래밍 언어에서 현재 표준으로 사용되는 Algol 스타일 스코프 모델의 완전한 유연성을 Python이 갖도록 하는 데 관심을 표명했습니다.
이러한 기능은 필요하지 않다는 주장도 있습니다. 다시 바인딩할 수 있는 바깥 변수를 변경 가능한 객체로 감싸서 시뮬레이션할 수 있기 때문입니다.:
class Namespace:
pass
def make_scoreboard(frame, score=0):
ns = Namespace()
ns.score = 0
label = Label(frame)
label.pack()
for i in [-10, -1, 1, 10]:
def increment(step=i):
ns.score = ns.score + step
label['text'] = ns.score
button = Button(frame, text='%+d' % i, command=increment)
button.pack()
return label
그러나 이 해결 방법은 기존 스코프의 단점만 부각합니다. 함수의 목적은 자체 네임스페이스에 코드를 캡슐화하는 것이므로, 프로그래머가 기존 지역 스코프의 부족한 기능을 보완하기 위해 추가 네임스페이스를 만들고, 각 이름이 실제 스코프에 속해야 하는지 시뮬레이션된 스코프에 속해야 하는지 결정해야 한다는 점은 유감스러워 보입니다.
또 다른 흔한 반론은 원하는 기능을 다소 더 장황하게 작성하더라도 클래스로 대신 구현할 수 있다는 것입니다. 이 반론에 대한 한 가지 반박은 다른 구현 방식이 존재한다는 사실이 지원되는 프로그래밍 구성 요소인 중첩 스코프를 기능적으로 불완전한 상태로 남겨 둘 이유가 되지 않는다는 것입니다. Python은 여러 프로그래밍 패러다임을 지원하고 이를 우아하게 통합함으로써 큰 강점과 실용적 유연성, 교육적 역량을 얻기 때문에 때때로 “다중 패러다임 언어”라고 불립니다.
스코프 지정 구문에 대한 제안은 PEP 227의 중첩 스코프 지원이 채택되기 훨씬 전인 1994년부터 Python-Dev에 등장했습니다 [1]. 당시 Guido의 응답은 다음과 같았습니다.
이는 CSNS [고전적 정적 중첩 스코프]를 도입하는 것에 위험할 정도로 가깝습니다. If 그렇게 한다면, 제안한 스코프 지정의 의미 체계는 괜찮아 보입니다. 그래도 저는 CSNS를 위한 필요성이 이러한 종류의 구성을 정당화할 만큼 충분하지 않다고 생각합니다 …
“바깥 이름 재바인딩 논의”는 PEP 227 이후 Python-Dev에서 너무 여러 번 다시 등장하여 익숙한 사건이 되었으며, 현재 형태로는 적어도 2003년부터 반복되어 왔습니다 [2]. 이러한 논의에서 제안된 언어 변경 중 어느 것도 아직 채택되지는 않았지만, Guido는 언어 변경을 고려할 가치가 있음을 인정했습니다 [12].
기타 언어
배경 정보를 제공하기 위해, 이 절에서는 다른 일부 언어가 중첩 스코프와 다시 바인딩을 처리하는 방식을 설명합니다.
JavaScript, Perl, Scheme, Smalltalk, GNU C, C# 2.0
이러한 언어는 스코프를 나타내기 위해 변수 선언을 사용합니다. JavaScript에서는 어휘적으로 스코프가 지정된 변수를 var 키워드로 선언하며, 선언되지 않은 변수 이름은 전역으로 간주합니다. Perl에서는 어휘적으로 스코프가 지정된 변수를 my 키워드로 선언하며, 선언되지 않은 변수 이름은 전역으로 간주합니다. Scheme에서는 모든 변수를 선언해야 합니다(define 또는 let, 혹은 형식 매개변수로 선언합니다). Smalltalk에서는 모든 블록이 수직 막대 사이에 지역 변수 이름 목록을 선언하는 것으로 시작할 수 있습니다. C와 C#에서는 모든 변수에 대해 타입 선언을 요구합니다. 이러한 모든 경우에 변수는 선언을 포함하는 스코프에 속합니다.
Ruby (1.8 기준)
Ruby는 Python처럼 변수 선언을 요구하지 않고 정적으로 중첩된 스코프를 지원하려는, 현재 널리 사용되는 유일한 다른 언어로 보이기 때문에 유익한 예이며, 따라서 특이한 해결책을 고안해야 합니다. Ruby의 함수는 다른 함수 정의를 포함할 수 있으며, 중괄호로 둘러싸인 코드 블록도 포함할 수 있습니다. 블록은 외부 변수에 접근할 수 있지만 중첩된 함수는 접근할 수 없습니다. 블록 내부에서 이름에 대한 할당은 외부 스코프에 이미 바인딩된 이름을 가리지 않는 경우에만 지역 변수 선언을 의미하며, 그렇지 않으면 할당은 외부 이름에 대한 재바인딩으로 해석됩니다. Ruby의 스코프 구문과 규칙은 또한 매우 오랫동안 논의되어 왔으며, Ruby 2.0에서는 변경될 가능성이 있어 보입니다 [28].
제안 개요
외부 스코프의 이름을 재바인딩하는 방법에 관해 Python-Dev에는 다양한 제안이 있었습니다. 이러한 제안은 모두 두 범주로 나뉩니다. 이름이 바인딩되는 스코프에 새로운 구문을 추가하는 방법과 이름이 사용되는 스코프에 새로운 구문을 추가하는 방법입니다.
바인딩(외부) 스코프의 새로운 구문
스코프 재정의 선언
이 범주의 제안은 모두 JavaScript의 var와 유사한 새로운 종류의 선언문을 제안합니다. 이를 위한 몇 가지 가능한 키워드가 제안되었습니다.
이러한 모든 제안에서 특정 스코프 S의 var x와 같은 선언은 S 안에 중첩된 스코프에서 x에 대한 모든 참조가 S에서 바인딩된 x를 참조하도록 합니다.
이 범주의 제안에 대한 가장 큰 반론은 함수 정의의 의미가 문맥에 따라 달라지게 된다는 점입니다. 함수 정의를 다른 블록 내부로 옮기면, 둘러싸는 블록의 선언으로 인해 함수 내 지역 이름 참조가 비지역 참조가 될 수 있습니다. Ruby 1.8의 블록에서는 실제로 이러한 일이 발생합니다. 다음 예에서 두 setter는 겉보기에는 동일하지만 서로 다른 효과를 냅니다:
setter1 = proc { | x | y = x } # y is local here
y = 13
setter2 = proc { | x | y = x } # y is nonlocal here
setter1.call(99)
puts y # prints 13
setter2.call(77)
puts y # prints 77
이 제안은 JavaScript와 Perl의 선언과 유사하지만 언어에 미치는 효과는 다르다는 점에 유의하십시오. 해당 언어에서는 선언되지 않은 변수가 기본적으로 전역인 반면, Python에서는 선언되지 않은 변수가 기본적으로 지역이기 때문입니다. 따라서 JavaScript나 Perl에서 함수를 다른 블록 내부로 옮기면 이전에 전역이었던 이름 참조의 스코프를 축소할 수 있을 뿐이지만, Python에서 이 제안을 적용하면 이전에 지역이었던 이름 참조의 스코프가 확장될 수 있습니다.
필수 변수 선언
더욱 급진적인 제안 [21]은 Python의 스코프 추측 관례를 완전히 제거하고, Scheme과 마찬가지로 모든 이름을 해당 이름이 바인딩될 스코프에서 선언하도록 요구하자고 제안합니다. 이 제안에 따르면 var x = 3은 x를 로컬 스코프에 속하도록 선언하는 동시에 바인딩하며, 반면 x = 3은 기존의 가시적인 x를 재바인딩합니다. var x 선언을 포함하는 바깥 스코프가 없는 문맥에서는 x = 3 문이 정적으로 불법이라고 판정됩니다.
이 제안은 단순하고 일관된 모델을 제공하지만, 기존의 모든 Python 코드와 호환되지 않습니다.
참조 스코프(내부 스코프)의 새로운 구문
이 범주에는 세 가지 종류의 제안이 있습니다.
바깥 스코프 참조 표현식
이러한 유형의 제안은 표현식에서 변수를 사용할 때 바깥 스코프의 변수를 참조하는 새로운 방법을 제안합니다. 이를 위해 제안된 구문 중 하나는 .x [7]이며, 이는 로컬 바인딩을 생성하지 않고 x를 참조합니다. 이 제안의 우려되는 점은 많은 문맥에서 x와 .x를 서로 바꾸어 사용할 수 있어 독자를 혼란스럽게 할 수 있다는 것입니다 [31]. 이와 밀접하게 관련된 아이디어는 상승할 스코프 수준의 수를 지정하기 위해 여러 개의 점을 사용하는 것이지만 [8], 대부분은 이를 오류가 발생하기 쉽다고 봅니다 [17].
재바인딩 연산자
이 제안은 이름을 로컬로 선언하지 않고 이름을 재바인딩하는 새로운 할당과 유사한 연산자를 제안합니다 [2]. x = 3 문은 x를 로컬 변수로 선언하는 동시에 3에 바인딩하는 반면, x := 3 문은 x를 로컬로 선언하지 않고 기존 바인딩을 변경합니다.
이는 단순한 해결책이지만, PEP 3099에 따르면 이미 거부되었습니다(아마도 이를 놓치거나 =와 혼동하기가 너무 쉽기 때문일 것입니다).
스코프 재정의 선언
이 범주의 제안은 이름이 로컬이 되는 것을 방지하는 새로운 종류의 내부 스코프 선언문을 제안합니다. 이 선언문은 global 문과 본질적으로 유사하지만, 이름이 최상위 모듈 수준 스코프의 바인딩을 참조하도록 하는 대신 가장 가까운 바깥 스코프의 바인딩을 참조하도록 합니다.
이 접근법은 익숙한 Python 구문과 병행된다는 점과 함수 정의에서 문맥 독립성을 유지한다는 점 때문에 매력적입니다.
이 접근법은 보안 및 디버깅 측면에서도 장점이 있습니다. 그 결과의 Python은 다른 중첩 스코프 언어의 기능과 일치할 뿐만 아니라, 방어적 프로그래밍에 더욱 적합하다고 주장할 수 있는 구문으로 이를 구현합니다. 대부분의 다른 언어에서는 선언이 기존 이름의 스코프를 제한하므로, 실수로 선언을 생략하면 예상보다 더 광범위하고(즉, 더 위험한) 효과가 발생할 수 있습니다. 이 제안을 적용한 Python에서는 선언을 추가하는 데 드는 추가 노력이 비로컬 효과의 증가한 위험과 일치합니다(즉, 저항이 가장 적은 경로가 더 안전한 경로입니다).
이러한 선언에 사용할 여러 표기법이 제안되었습니다.
scoped x[1]global x in f[3] (어떤 스코프인지 명시적으로 지정)free x[5]outer x[6]use x[9]global x[10] (global의 의미 변경)nonlocal x[11]global x outer[18]global in x[18]not global x[18]extern x[20]ref x[22]refer x[22]share x[22]sharing x[22]common x[22]using x[22]borrow x[22]reuse x[23]scope f x[25] (어떤 스코프인지 명시적으로 지정합니다)
가장 일반적으로 논의되는 선택지는 outer, global, nonlocal인 것으로 보입니다. outer는 표준 라이브러리에서 이미 변수 이름과 속성 이름으로 모두 사용되고 있습니다. global이라는 단어는 의미가 충돌합니다. “전역 변수”는 일반적으로 최상위 스코프를 가진 변수로 이해되기 때문입니다 [27]. C에서 extern 키워드는 어떤 이름이 다른 컴파일 단위의 변수를 가리킨다는 의미입니다. nonlocal은 다른 선택지보다 조금 길고 발음도 덜 유쾌하지만, 정확히 올바른 의미를 지닙니다. 즉, 이름을 로컬이 아닌 것으로 선언합니다.
제안된 해결책
이 PEP에서 제안하는 해결책은 이름을 참조하는 (내부) 스코프에 스코프 재정의 선언을 추가하는 것입니다. Guido는 Python-Dev에서 이러한 종류의 해결책을 선호한다고 밝혔으며 [14], 키워드로 nonlocal을 사용하는 것에 대한 지지도 표명했습니다 [19].
제안된 선언:
nonlocal x
현재 스코프에서 x가 로컬 이름이 되는 것을 방지합니다. 현재 스코프에서 x의 모든 출현은 바깥쪽의 둘러싸는 스코프에 바인딩된 x를 참조합니다. global과 마찬가지로 여러 이름을 지정할 수 있습니다.:
nonlocal x, y, z
둘러싸는 스코프에 기존 바인딩이 없으면 컴파일러는 SyntaxError를 발생시킵니다. (이를 구문 오류라고 부르는 것은 다소 무리일 수 있지만, 지금까지 SyntaxError는 알 수 없는 기능 이름을 사용한 __future__ import와 같은 경우를 포함하여 모든 컴파일 시간 오류에 사용되었습니다.) Guido는 바깥쪽 바인딩이 없는 상태에서 이러한 종류의 선언을 사용하는 것은 오류로 간주해야 한다고 말했습니다 [16].
nonlocal 선언이 로컬 스코프의 형식 매개변수 이름과 충돌하면 컴파일러는 SyntaxError를 발생시킵니다.
nonlocal을 대입문 또는 확장 대입문 앞에 붙이는 축약 형식도 허용됩니다.:
nonlocal x = 3
위의 내용은 nonlocal x; x = 3과 정확히 같은 의미입니다. (Guido는 global문에도 유사한 형식을 지원합니다 [24].)
축약 형식의 왼쪽에는 식별자만 허용되며, x[0]과 같은 대상 표현식은 허용되지 않습니다. 그 밖에는 모든 형태의 대입이 허용됩니다. 제안된 nonlocal 문의 문법은 다음과 같습니다.:
nonlocal_stmt ::=
"nonlocal" identifier ("," identifier)*
["=" (target_list "=")+ expression_list]
| "nonlocal" identifier augop expression_list
이러한 모든 형태의 대입을 허용하는 근거는 nonlocal 문을 더 쉽게 이해할 수 있게 한다는 것입니다. 축약 형식을 선언과 대입으로 분리하면 그 의미와 유효성을 이해하기에 충분합니다.
하위 호환성
이 PEP는 Guido [19]가 제안한 대로 Python 3000을 대상으로 합니다. 그러나 다른 사람들은 이 PEP에서 고려된 일부 선택 사항이 Python 2.x [26] 에서 구현할 수 있을 만큼 변경 폭이 작을 수 있다고 지적했으며, 그런 경우 이 PEP를 2.x 계열 PEP로 옮길 수도 있습니다.
새 키워드를 도입하는 영향의 (매우 대략적인) 척도로, 2006년 11월 5일 Python SVN 저장소를 검사한 결과에 따르면 제안된 키워드 중 일부가 표준 라이브러리에서 식별자로 나타나는 횟수는 다음과 같습니다.:
nonlocal 0
use 2
using 3
reuse 4
free 8
outer 147
global은 기존 키워드로 214회 나타납니다. 외부 스코프 키워드로 global을 사용하는 영향의 척도로, 이러한 변경의 결과 표준 라이브러리의 18개 파일이 중단됩니다(함수가 해당 변수가 전역 스코프에 도입되기 전에 변수 global을 선언하기 때문입니다).:
cgi.py
dummy_thread.py
mhlib.py
mimetypes.py
idlelib/PyShell.py
idlelib/run.py
msilib/__init__.py
test/inspect_fodder.py
test/test_compiler.py
test/test_decimal.py
test/test_descr.py
test/test_dummy_threading.py
test/test_fileinput.py
test/test_global.py (not counted: this tests the keyword itself)
test/test_grammar.py (not counted: this tests the keyword itself)
test/test_itertools.py
test/test_multifile.py
test/test_scope.py (not counted: this tests the keyword itself)
test/test_threaded_import.py
test/test_threadsignals.py
test/test_warnings.py
참고 문헌
[15] 명시적 렉시컬 스코핑 (PEP 이전?) (Guido van Rossum) https://mail.python.org/pipermail/python-dev/2006-July/066995.html
감사의 말
이 PEP에서 언급한 아이디어와 제안은 수많은 Python-Dev 게시물에서 얻은 것입니다. 이 PEP에 대한 구체적인 수정 사항을 제안해 주신 Jim Jewett, Mike Orr, Jason Orendorff, Christian Tanzer에게 감사드립니다.
Copyright
This document has been placed in the public domain.