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 사용자와 개발자들은 이 제약을 없애서 Python도 JavaScript, Perl, Ruby, Scheme, Smalltalk, GNU 확장이 적용된 C, C# 2.0 등 오늘날 많은 프로그래밍 언어에서 표준이 된 Algol 방식 스코프 모델의 유연성을 온전히 갖추기를 바란다는 관심을 표명해 왔습니다.
다시 바인딩할 수 있는 바깥 변수는 가변 객체로 감싸서 흉내 낼 수 있으므로 이런 기능은 필요하지 않다는 주장도 있었습니다:
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 [고전적 정적 중첩 스코프]를 도입하는 것에 위험할 정도로 가깝습니다. 만약 그렇게 한다면, 제안하신 스코프 의미 체계는 괜찮아 보입니다. 저는 여전히 이런 구문을 정당화할 만큼 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의 스코프 문법과 규칙 역시 오랫동안 많은 논쟁의 대상이 되었으며, 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와 .x를 서로 바꾸어 쓸 수 있어 코드를 읽는 사람을 혼란스럽게 한다는 점입니다 [31]. 이와 밀접하게 관련된 아이디어로 점을 여러 개 사용해 거슬러 올라갈 스코프 단계 수를 지정하는 방법이 있지만 [8], 대부분은 이 방법이 너무 오류를 일으키기 쉽다고 생각합니다 [17].
재바인딩 연산자
이 제안은 이름을 지역으로 선언하지 않으면서 그 이름을 다시 바인딩하는, 대입과 비슷한 새 연산자를 제안합니다 [2]. x = 3 문은 x를 지역 변수로 선언하는 동시에 3에 바인딩하지만, x := 3 문은 x를 지역으로 선언하지 않고 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__ 임포트도 여기에 포함됩니다.) 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] Explicit Lexical Scoping (pre-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.