[Final] PEP 3104 - Access to Names in Outer Scopes
원문 링크: PEP 3104 - Access to Names in Outer Scopes
상태: Final 유형: Standards Track 작성일: 12-Oct-2006
PEP 3104 – 외부 스코프의 이름 접근
작성자: Ka-Ping Yee
요약 (Abstract)
대부분의 중첩 스코프(nested scopes)를 지원하는 언어에서 코드는 가장 가까운 바깥쪽 스코프(enclosing scope)에 있는 이름을 참조하거나 재할당(rebind, 값 할당)할 수 있습니다. 현재 Python 코드는 어떤 바깥쪽 스코프에 있는 이름이든 참조할 수 있지만, 이름을 재할당할 수 있는 스코프는 두 가지뿐입니다. 즉, 지역 스코프(local scope) (단순 할당 사용) 또는 모듈-전역 스코프(module-global scope) ( global
선언 사용) 입니다.
이러한 제한은 Python-Dev 메일링 리스트 및 다른 곳에서 여러 번 제기되었으며, 이 제한을 제거하기 위한 확장된 논의와 많은 제안으로 이어졌습니다. 이 PEP는 제시된 다양한 대안들과 각각의 장단점을 요약합니다.
배경 (Rationale)
버전 2.1 이전에는 Python의 스코프 처리가 표준 C와 유사했습니다. 즉, 파일 내에는 전역(global)과 지역(local)이라는 두 가지 스코프 레벨만 있었습니다. C에서 이는 함수 정의가 중첩될 수 없다는 사실의 자연스러운 결과입니다. 그러나 Python에서는 함수가 일반적으로 최상위 수준에서 정의되더라도 함수 정의는 어디에서나 실행될 수 있습니다. 이로 인해 Python은 의미론 없이 중첩 스코프의 구문적 형태를 가지게 되었고, 일부 프로그래머들에게는 놀라운 불일치를 초래했습니다. 예를 들어, 최상위 수준에서 작동하던 재귀 함수가 다른 함수 내로 이동되면 작동을 멈추게 되었는데, 이는 재귀 함수 자체의 이름이 더 이상 해당 함수의 본문 스코프에서 보이지 않았기 때문입니다. 이는 함수가 다른 컨텍스트에 배치될 때 일관되게 동작해야 한다는 직관을 위반합니다. 다음은 예시입니다.
def enclosing_function():
def factorial(n):
if n < 2:
return 1
return n * factorial(n - 1) # NameError 발생
print(factorial(5))
Python 2.1은 모든 바깥쪽 스코프에 바인딩된 이름을 보이게 함으로써 정적 중첩 스코프(static nested scoping)에 더 가까워졌습니다 (PEP 227 참조). 이 변경으로 위 코드 예시가 예상대로 작동하게 되었습니다. 그러나 이름에 대한 모든 할당이 해당 이름을 지역(local)으로 암시적으로 선언하기 때문에, 바깥쪽 스코프의 이름을 재할당하는 것은 불가능합니다 ( global
선언이 이름을 전역(global)으로 강제하는 경우 제외). 따라서 다음 코드는 버튼 클릭으로 증가 및 감소할 수 있는 숫자를 표시하도록 의도되었지만, 어휘적 스코프(lexical scoping)에 익숙한 사람이라면 예상하는 대로 작동하지 않습니다.
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 # UnboundLocalError 발생
label['text'] = score
button = Button(frame, text='%+d' % i, command=increment)
button.pack()
return label
Python 구문은 increment
내에서 언급된 score
라는 이름이 make_scoreboard
에 바인딩된 score
변수를 참조하며, increment
내의 지역 변수가 아님을 나타내는 방법을 제공하지 않습니다. 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에 등장했습니다. 당시 Guido의 답변은 “이것은 CSNS(classic static nested scopes)를 도입하는 데 위험할 정도로 가깝습니다. 그렇게 한다면, scoped
의 제안된 의미론은 괜찮아 보입니다. 나는 여전히 이런 종류의 구성을 정당화할 만큼 CSNS의 필요성이 충분하지 않다고 생각합니다…“였습니다.
PEP 227 이후, “외부 이름 재할당 논의”는 Python-Dev에 충분히 자주 다시 등장하여 친숙한 이벤트가 되었으며, 최소 2003년부터 현재 형태로 재발했습니다. 이러한 논의에서 제안된 언어 변경 사항 중 채택된 것은 없지만, Guido는 언어 변경이 고려할 가치가 있음을 인정했습니다.
다른 언어 (Other Languages)
일부 다른 언어들이 중첩 스코프와 재할당을 어떻게 처리하는지 배경 설명을 위해 이 섹션에서 다룹니다.
JavaScript, Perl, Scheme, Smalltalk, GNU C, C# 2.0
이 언어들은 변수 선언을 사용하여 스코프를 나타냅니다. JavaScript에서는 var
키워드로 어휘적으로 스코프가 지정된 변수를 선언합니다. 선언되지 않은 변수 이름은 전역(global)으로 간주됩니다. Perl에서는 my
키워드로 어휘적으로 스코프가 지정된 변수를 선언합니다. 선언되지 않은 변수 이름은 전역으로 간주됩니다. Scheme에서는 모든 변수를 선언해야 합니다 (define
또는 let
을 사용하거나 형식 매개변수로). Smalltalk에서는 모든 블록이 세로 막대 사이에 지역 변수 이름 목록을 선언하여 시작할 수 있습니다. C 및 C#은 모든 변수에 대해 타입 선언을 요구합니다. 이 모든 경우에 변수는 선언을 포함하는 스코프에 속합니다.
Ruby (1.8 기준)
Ruby는 Python과 마찬가지로 변수 선언을 요구하지 않고 정적으로 중첩된 스코프를 지원하려고 시도하며, 따라서 특이한 해결책을 찾아야 한다는 점에서 유익한 예시입니다. Ruby의 함수는 다른 함수 정의를 포함할 수 있으며, 중괄호로 묶인 코드 블록도 포함할 수 있습니다. 블록은 외부 변수에 접근할 수 있지만, 중첩된 함수는 그렇지 않습니다. 블록 내에서 이름에 대한 할당은 외부 스코프에 이미 바인딩된 이름을 가리지 않는 경우에만 지역 변수의 선언을 의미합니다. 그렇지 않으면 할당은 외부 이름의 재할당으로 해석됩니다. Ruby의 스코핑 구문과 규칙 또한 오랫동안 논의되어 왔으며, Ruby 2.0에서 변경될 가능성이 있습니다.
제안 개요 (Overview of Proposals)
Python-Dev에는 외부 스코프의 이름을 재할당하는 방법에 대한 많은 다른 제안들이 있었습니다. 이들은 모두 두 가지 범주로 나뉩니다. 이름이 바인딩되는 스코프(바깥쪽 스코프)에 새로운 구문을 추가하거나, 이름이 사용되는 스코프(안쪽 스코프)에 새로운 구문을 추가하는 것입니다.
바인딩(바깥쪽) 스코프의 새로운 구문 (New Syntax in the Binding (Outer) Scope)
스코프 재정의 선언 (Scope Override Declaration)
이 범주의 제안들은 모두 JavaScript의 var
와 유사한 새로운 종류의 선언문을 제안합니다. 이 목적을 위해 몇 가지 가능한 키워드가 제안되었습니다.
scope x
var x
my x
이 모든 제안에서 특정 스코프 S
내의 var x
와 같은 선언은 S
내에 중첩된 스코프에서 x
에 대한 모든 참조가 S
에 바인딩된 x
를 참조하도록 합니다.
이 범주의 제안에 대한 주요 반대는 함수 정의의 의미가 컨텍스트에 민감해진다는 것입니다. 함수 정의를 다른 블록 안으로 이동하면, 둘러싸는 블록의 선언 때문에 함수 내의 지역 이름 참조 중 일부가 비지역(nonlocal)이 될 수 있습니다. Ruby 1.8의 블록의 경우 실제로 그러합니다. 다음 예시에서 두 setter
는 동일하게 보이지만 다른 효과를 가집니다.
setter1 = proc { | x | y = x } # y는 여기서는 지역 변수
y = 13
setter2 = proc { | x | y = x } # y는 여기서는 비지역 변수
setter1.call(99)
puts y # 13을 출력
setter2.call(77)
puts y # 77을 출력
이 제안이 JavaScript 및 Perl의 선언과 유사하지만, 언어에 미치는 영향은 다릅니다. 이 언어들에서는 선언되지 않은 변수가 기본적으로 전역인 반면, Python에서는 선언되지 않은 변수가 기본적으로 지역이기 때문입니다. 따라서 JavaScript 또는 Perl에서 함수를 다른 블록 안으로 이동하면 이전에 전역이었던 이름 참조의 스코프를 줄일 수 있을 뿐이지만, 이 제안이 있는 Python에서는 이전에 지역이었던 이름 참조의 스코프를 확장할 수 있습니다.
필수 변수 선언 (Required Variable Declaration)
더 급진적인 제안은 Python의 스코프 추측 규칙을 완전히 제거하고, Scheme처럼 모든 이름이 바인딩될 스코프에서 선언되어야 한다고 제안합니다. 이 제안에서 var x = 3
은 x
가 지역 스코프에 속하고 3
으로 바인딩되도록 선언하는 반면, x = 3
은 기존에 보이는 x
를 재할당합니다. var x
선언을 포함하는 바깥쪽 스코프가 없는 컨텍스트에서는 x = 3
문이 정적으로 불법으로 결정됩니다.
이 제안은 간단하고 일관된 모델을 제공하지만, 기존의 모든 Python 코드와 호환되지 않을 것입니다.
참조(안쪽) 스코프의 새로운 구문 (New Syntax in the Referring (Inner) Scope)
이 범주에는 세 가지 유형의 제안이 있습니다.
외부 참조 표현식 (Outer Reference Expression)
이 유형의 제안은 변수를 표현식에서 사용할 때 외부 스코프의 변수를 참조하는 새로운 방법을 제안합니다. 이를 위해 제안된 한 가지 구문은 .x
이며, 이는 지역 바인딩을 생성하지 않고 x
를 참조합니다. 이 제안에 대한 우려는 많은 컨텍스트에서 x
와 .x
가 상호 교환적으로 사용될 수 있어 독자를 혼란스럽게 할 수 있다는 것입니다. 밀접하게 관련된 아이디어는 스코프 레벨을 올라갈 수를 지정하기 위해 여러 점을 사용하는 것이지만, 대부분은 이것이 너무 오류를 유발하기 쉽다고 간주합니다.
재할당 연산자 (Rebinding Operator)
이 제안은 이름을 지역으로 선언하지 않고 이름을 재할당하는 새로운 할당 유사 연산자를 제안합니다. x = 3
문이 x
를 지역 변수로 선언하고 3
으로 바인딩하는 반면, x := 3
문은 x
의 기존 바인딩을 변경하되 지역으로 선언하지 않습니다.
이것은 간단한 해결책이지만, PEP 3099에 따르면 거부되었습니다 (아마도 놓치기 쉽거나 =
와 혼동하기 쉽기 때문일 수 있습니다).
스코프 재정의 선언 (Scope Override Declaration)
이 범주의 제안들은 안쪽 스코프에서 이름이 지역이 되는 것을 방지하는 새로운 종류의 선언문을 제안합니다. 이 문은 global
문과 본질적으로 유사하지만, 이름을 최상위 모듈-레벨 스코프의 바인딩을 참조하게 하는 대신, 가장 가까운 바깥쪽 스코프의 바인딩을 참조하게 합니다.
이 접근 방식은 익숙한 Python 구성 요소와 유사하며 함수 정의에 대한 컨텍스트 독립성을 유지한다는 점에서 매력적입니다.
이 접근 방식은 보안 및 디버깅 관점에서도 장점이 있습니다. 결과적인 Python은 다른 중첩 스코프 언어의 기능과 일치할 뿐만 아니라, 방어적 프로그래밍에 arguably 더 나은 구문을 제공할 것입니다. 대부분의 다른 언어에서는 선언이 기존 이름의 스코프를 축소하므로, 부주의하게 선언을 생략하면 예상보다 광범위한 (즉, 더 위험한) 효과를 초래할 수 있습니다. 이 제안이 있는 Python에서는 선언을 추가하는 추가적인 노력이 비지역 효과의 증가된 위험과 일치합니다 (즉, 가장 적은 저항의 경로가 더 안전한 경로입니다).
이러한 선언에 대해 많은 표기법이 제안되었습니다.
scoped x
global x in f
(어떤 스코프인지 명시적으로 지정)free x
outer x
use x
global x
(global
의 의미 변경)nonlocal x
global x outer
global in x
not global x
extern x
ref x
refer x
share x
sharing x
common x
using x
borrow x
reuse x
scope f x
(어떤 스코프인지 명시적으로 지정)
가장 일반적으로 논의된 선택은 outer
, global
, nonlocal
이었습니다. outer
는 표준 라이브러리에서 변수 이름과 속성 이름으로 모두 사용됩니다. global
이라는 단어는 “전역 변수”가 일반적으로 최상위 스코프의 변수를 의미하는 것으로 이해되기 때문에 의미가 충돌합니다. C에서 extern
키워드는 이름이 다른 컴파일 단위의 변수를 참조한다는 의미입니다. nonlocal
은 약간 길고 다른 옵션보다 덜 듣기 좋지만, “지역이 아닌”이라는 정확한 의미를 가집니다.
제안된 해결책 (Proposed Solution)
이 PEP에서 제안하는 해결책은 참조(안쪽) 스코프에 스코프 재정의 선언을 추가하는 것입니다. Guido는 Python-Dev에서 이 범주의 해결책에 대한 선호를 표명했으며, nonlocal
을 키워드로 승인했습니다.
제안된 선언은 다음과 같습니다.
nonlocal x
이는 현재 스코프에서 x
가 지역 이름이 되는 것을 방지합니다. 현재 스코프에서 x
의 모든 발생은 바깥쪽 둘러싸는 스코프에 바인딩된 x
를 참조하게 됩니다. global
과 마찬가지로 여러 이름이 허용됩니다.
nonlocal x, y, z
둘러싸는 스코프에 기존 바인딩이 없으면 컴파일러는 SyntaxError
를 발생시킵니다. (이를 SyntaxError
라고 부르는 것은 다소 무리가 있을 수 있지만, 현재까지 SyntaxError
는 알 수 없는 기능 이름과 함께 __future__ import
를 포함하여 모든 컴파일 시간 오류에 사용됩니다.) Guido는 외부 바인딩이 없는 이런 종류의 선언은 오류로 간주되어야 한다고 말했습니다.
nonlocal
선언이 지역 스코프의 형식 매개변수 이름과 충돌하면 컴파일러는 SyntaxError
를 발생시킵니다.
단축 형태도 허용됩니다. 이 경우 nonlocal
이 할당 또는 증강 할당 앞에 붙습니다.
nonlocal x = 3
위의 내용은 nonlocal x; x = 3
과 정확히 같은 의미를 가집니다. (Guido는 global
문의 유사한 형태를 지지합니다.)
단축 형태의 왼쪽에는 식별자만 허용되며, x[0]
와 같은 대상 표현식은 허용되지 않습니다. 그 외의 모든 할당 형태는 허용됩니다. nonlocal
문의 제안된 문법은 다음과 같습니다.
nonlocal_stmt ::= "nonlocal" identifier ("," identifier)* ["=" (target_list "=")+ expression_list]
| "nonlocal" identifier augop expression_list
이 모든 할당 형태를 허용하는 이유는 nonlocal
문의 이해를 단순화하기 위함입니다. 단축 형태를 선언과 할당으로 분리하는 것이 그 의미와 유효성을 이해하는 데 충분합니다.
참고: 원본 PEP 구현에는 단축 구문이 추가되지 않았습니다. 이후 논의에서는 이 구문을 구현해서는 안 된다는 결론을 내렸습니다.
하위 호환성 (Backward Compatibility)
이 PEP는 Guido가 제안한 대로 Python 3000(Python 3.x의 초기 명칭)을 대상으로 합니다. 그러나 다른 이들은 이 PEP에서 고려된 일부 옵션이 Python 2.x에서 실행 가능한 충분히 작은 변경 사항일 수 있으며, 이 경우 이 PEP가 2.x 시리즈 PEP로 이동될 수도 있다고 언급했습니다.
새로운 키워드를 도입하는 영향을 (매우 대략적으로) 측정하기 위해, 2006년 11월 5일 Python SVN 저장소 스캔에 따르면 표준 라이브러리에서 제안된 일부 키워드가 식별자로 나타난 횟수는 다음과 같습니다.
nonlocal
: 0use
: 2using
: 3reuse
: 4free
: 8outer
: 147
global
은 기존 키워드로 214번 나타납니다. global
을 외부 스코프 키워드로 사용하는 영향을 측정하면, 그러한 변경으로 인해 표준 라이브러리에서 깨질 파일은 18개입니다 (함수가 전역 스코프에 해당 변수가 도입되기 전에 변수를 global
로 선언하기 때문).
참고 문헌 (References)
Scoping (was Re: Lambda binding solved?) (Rafael Bracho) Extended Function syntax (Just van Rossum) Closure semantics (Guido van Rossum) … (생략, 원문 참조) …
감사의 글 (Acknowledgements)
이 PEP에 언급된 아이디어와 제안들은 수많은 Python-Dev 게시물에서 얻은 것입니다. 이 PEP에 대한 특정 편집을 제안해 주신 Jim Jewett, Mike Orr, Jason Orendorff, Christian Tanzer에게 감사드립니다.
저작권 (Copyright)
이 문서는 퍼블릭 도메인에 공개되었습니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments