[Final] PEP 227 - Statically Nested Scopes
원문 링크: PEP 227 - Statically Nested Scopes
상태: Final 유형: Standards Track 작성일: 01-Nov-2000
PEP 227 – 정적으로 중첩된 스코프 (Statically Nested Scopes) 번역 및 요약
초록 (Abstract)
이 PEP는 Python 2.2에 정적으로 중첩된 스코프(Lexical Scoping)를 추가하고, Python 2.1에서는 소스 레벨 옵션으로 제공하는 내용을 기술합니다. 또한, Python 2.1은 이 기능이 활성화될 때 의미가 변경될 수 있는 구문에 대해 경고를 발행합니다.
기존 Python 2.0 및 이전 버전의 언어 정의는 이름을 해석하는 데 사용되는 세 가지 네임스페이스(local, global, built-in)만 정의했습니다. 중첩된 스코프의 추가는 바인딩되지 않은(unbound) 지역 이름이 둘러싸는(enclosing) 함수의 네임스페이스에서 해석될 수 있도록 합니다.
이 변경의 가장 두드러진 결과는 lambda
(및 다른 중첩 함수)가 주변 네임스페이스에 정의된 변수를 참조할 수 있게 된다는 것입니다. 이전에는 lambda
가 자신의 네임스페이스에 명시적으로 바인딩을 생성하기 위해 종종 기본 인수를 사용해야 했습니다.
서론 (Introduction)
이 제안은 Python 함수에서 자유 변수(free variables)를 해석하는 규칙을 변경합니다. 새로운 이름 해석 의미론은 Python 2.2부터 적용되며, Python 2.1에서는 모듈 상단에 from __future__ import nested_scopes
를 추가하여 사용할 수 있습니다.
Python 2.0 정의는 각 이름을 확인하기 위해 정확히 세 가지 네임스페이스(local, global, builtin)를 지정했습니다. 이 정의에 따르면, 함수 A가 함수 B 내부에 정의된 경우, B에 바인딩된 이름은 A에서 볼 수 없었습니다. 이 제안은 규칙을 변경하여 A에 이름 바인딩이 B의 바인딩을 숨기지 않는 한, B에 바인딩된 이름이 A에서 보이도록 합니다.
이 사양은 Algol-like 언어에서 흔히 볼 수 있는 렉시컬 스코프(lexical scoping) 규칙을 도입합니다. 렉시컬 스코프와 기존의 일급 함수(first-class functions) 지원의 조합은 Scheme을 연상시킵니다.
변경될 스코프 규칙은 두 가지 문제를 해결합니다. 첫째는 lambda
표현식(및 일반적으로 중첩 함수)의 제한된 유용성이고, 둘째는 다른 중첩 렉시컬 스코프를 지원하는 언어에 익숙한 신규 사용자의 혼란(예: 모듈 수준에서만 재귀 함수를 정의할 수 있는 것)입니다.
lambda
표현식은 단일 표현식을 평가하는 익명 함수를 생성하며, 종종 콜백 함수로 사용됩니다. Python 2.0 규칙을 사용한 예시에서는, lambda
본문에서 사용되는 모든 이름은 lambda
에 기본 인수로 명시적으로 전달되어야 했습니다. 예를 들어, lambda root=root: root.test.configure(text="...")
와 같이 root=root
를 명시해야 했습니다. 이 방식은 번거롭고, 특히 lambda
본문에서 여러 이름이 사용될 때 코드의 목적을 모호하게 만들었습니다. 제안된 해결책은 이러한 기본 인수 접근 방식을 자동으로 구현하여 “root=root” 인수를 생략할 수 있게 합니다.
새로운 이름 해석 의미론은 일부 프로그램이 Python 2.0과 다르게 동작하게 만들 것입니다. 어떤 경우에는 컴파일에 실패하고, 어떤 경우에는 이전에 전역 네임스페이스를 사용하여 해석되었던 이름이 둘러싸는 함수의 지역 네임스페이스를 사용하여 해석됩니다. Python 2.1에서는 다르게 동작할 모든 구문에 대해 경고가 발행됩니다.
사양 (Specification)
Python은 Algol과 같이 블록 구조를 가진 정적 스코프 언어입니다. 모듈, 클래스 정의 또는 함수 본문과 같은 코드 블록 또는 영역이 프로그램의 기본 단위입니다.
이름은 객체를 참조합니다. 이름은 이름 바인딩 작업에 의해 도입됩니다. 프로그램 텍스트에서 이름이 나타날 때마다 해당 이름은 사용을 포함하는 가장 안쪽 함수 블록에 설정된 해당 이름의 바인딩을 참조합니다.
이름 바인딩 작업에는 인수 선언, 할당, 클래스 및 함수 정의, import
문, for
문, except
절이 있습니다. 각 이름 바인딩은 클래스 또는 함수 정의에 의해 정의된 블록 내에서 또는 모듈 수준(최상위 코드 블록)에서 발생합니다.
이름이 코드 블록 내의 어디에서든 바인딩되면, 블록 내의 모든 이름 사용은 현재 블록에 대한 참조로 처리됩니다. (global
문이 블록 내에서 사용되면, 해당 문에 지정된 이름의 모든 사용은 최상위 네임스페이스의 이름 바인딩을 참조합니다.) 이름이 코드 블록 내에서 사용되지만 바인딩되지 않고 global
로 선언되지 않은 경우, 해당 사용은 가장 가까운 둘러싸는 함수 영역에 대한 참조로 처리됩니다.
클래스 스코프의 이름은 중첩된 함수에서 접근할 수 없습니다. 즉, 이름 해석 과정에서 클래스 정의는 건너뜁니다. global
문은 일반적인 규칙을 단축합니다. 변수는 선언되지 않습니다. 함수 내의 어디에서든 이름 바인딩 작업이 발생하면 해당 이름은 함수에 지역적인 것으로 간주되며 모든 참조는 지역 바인딩을 참조합니다.
del
문을 사용하여 둘러싸는 스코프에서 참조되는 변수를 삭제하는 것은 SyntaxError
를 발생시킵니다.
함수 내에서 import *
가 사용되고 해당 함수에 자유 변수를 가진 중첩 블록이 포함된 경우, 컴파일러는 SyntaxError
를 발생시킵니다.
함수 내에서 exec
가 사용되고 해당 함수에 자유 변수를 가진 중첩 블록이 포함된 경우, exec
가 명시적으로 로컬 네임스페이스를 지정하지 않으면 컴파일러는 SyntaxError
를 발생시킵니다.
함수 스코프에 바인딩된 이름이 모듈 전역 이름 또는 표준 빌트인 이름과 동일하고, 함수에 해당 이름을 참조하는 중첩 함수 스코프가 포함된 경우, 컴파일러는 경고를 발행합니다.
논의 (Discussion)
제안된 규칙은 함수에 정의된 이름이 해당 함수 내에 정의된 모든 중첩 함수에서 참조될 수 있도록 합니다. 이름 해석 규칙은 정적 스코프 언어의 일반적인 특징이지만, 세 가지 주요 예외가 있습니다:
- 클래스 스코프의 이름은 접근할 수 없습니다. 이름은 가장 안쪽의 둘러싸는 함수 스코프에서 해석됩니다. 클래스 정의가 중첩 스코프 체인에 있을 경우, 해석 과정은 클래스 정의를 건너뜁니다. 이 규칙은 클래스 속성과 지역 변수 접근 간의 이상한 상호작용을 방지합니다. 클래스 정의 내에서 이름 바인딩 작업이 발생하면, 결과 클래스 객체에 속성을 생성합니다. 메서드 또는 메서드 내에 중첩된 함수에서 이 변수에 접근하려면
self
또는 클래스 이름을 통한 속성 참조를 사용해야 합니다. global
문은 일반적인 규칙을 단축시킵니다. 이 제안 하에서global
문은 Python 2.0과 정확히 동일한 효과를 가집니다. 이는 한 블록에서 수행된 이름 바인딩 작업이 다른 블록(모듈)의 바인딩을 변경할 수 있도록 한다는 점에서도 주목할 만합니다.- 변수는 선언되지 않습니다. 함수 내의 어디에서든 이름 바인딩 작업이 발생하면, 해당 이름은 함수에 지역적인 것으로 간주되며 모든 참조는 지역 바인딩을 참조합니다. 이름이 바인딩되기 전에 참조가 발생하면
NameError
가 발생합니다. 유일한 선언 유형은global
문으로, 변경 가능한 전역 변수를 사용하여 프로그램을 작성할 수 있도록 합니다. 결과적으로, 둘러싸는 스코프에 정의된 이름을 다시 바인딩하는 것은 불가능합니다. 할당 작업은 현재 스코프 또는 전역 스코프에서만 이름을 바인딩할 수 있습니다. 선언의 부재와 둘러싸는 스코프의 이름을 다시 바인딩할 수 없다는 점은 렉시컬 스코프 언어에서는 이례적입니다.
예시 (Examples)
PEP 227은 make_adder
와 같은 함수 클로저를 통해 외부 스코프의 변수를 참조하는 예시를 보여줍니다. 이 예시에서 adder
함수는 make_adder
함수의 base
변수를 기억하고 사용할 수 있습니다.
def make_adder(base):
def adder(x):
return base + x
return adder
add5 = make_adder(5)
print(add5(6)) # 출력: 11
또한, 중첩 스코프와 관련된 잠재적인 문제점도 제시합니다. 특히, 둘러싸는 스코프에서 반복문을 통해 변수가 지역적으로 재정의될 경우, 중첩된 함수가 해당 지역 변수를 참조하게 되어 예상치 못한 동작을 야기할 수 있습니다.
하위 호환성 (Backwards compatibility)
중첩 스코프로 인해 두 가지 유형의 호환성 문제가 발생합니다. 하나는 이전 버전에서 다르게 동작하던 코드가 중첩 스코프로 인해 다르게 동작하는 경우이고, 다른 하나는 특정 구문이 중첩 스코프와 제대로 상호작용하지 않아 컴파일 시 SyntaxError
를 유발하는 경우입니다.
첫 번째 문제의 예시는 다음과 같습니다.
x = 1
def f1():
x = 2
def inner():
print(x)
inner()
Python 2.0 규칙에서는 inner()
내부의 print x
는 전역 변수 x
를 참조하여 1을 출력했지만, 새 규칙에서는 f1()
의 네임스페이스(가장 가까운 둘러싸는 스코프)를 참조하여 2를 출력합니다. 이러한 문제는 전역 변수와 지역 변수가 같은 이름을 공유하고 중첩 함수가 그 이름을 전역 변수를 참조하는 데 사용할 때만 발생합니다. 이는 좋은 프로그래밍 습관이 아니며, Python 2.1 컴파일러는 이러한 경우 경고를 발행합니다.
두 번째 호환성 문제는 함수 본문에서 import *
또는 exec
를 사용하고 해당 함수에 자유 변수를 가진 중첩 스코프가 포함된 경우에 발생합니다. 컴파일러는 exec
또는 import *
가 전역 y
를 가리는 이름 바인딩을 도입할지 여부를 알 수 없으므로, 중첩 함수에서 y
에 대한 참조가 전역을 참조해야 할지 아니면 둘러싸는 함수의 지역 이름을 참조해야 할지 판단할 수 없습니다. 이러한 모호성 때문에 컴파일러는 예외를 발생시키고, Python 2.1 컴파일러는 중첩 스코프가 활성화되지 않은 경우 경고를 발행합니다.
C API 호환성 (Compatibility of C API)
이 구현으로 인해 PyCode_New()
를 포함한 여러 Python C API 함수가 변경됩니다. 결과적으로 C 확장 모듈은 Python 2.1과 올바르게 작동하려면 업데이트가 필요할 수 있습니다.
locals()
/ vars()
이 함수들은 현재 스코프의 지역 변수를 포함하는 딕셔너리를 반환합니다. 딕셔너리 수정은 변수 값에 영향을 미치지 않습니다. 기존 규칙에서는 locals()
와 globals()
를 사용하여 이름이 해석되는 모든 네임스페이스에 접근할 수 있었습니다.
중첩 스코프를 위한 유사한 함수는 제공되지 않습니다. 이 제안 하에서는 보이는 모든 스코프에 딕셔너리 스타일로 접근하는 것이 불가능할 것입니다.
경고 및 오류 (Warnings and Errors)
Python 2.1 컴파일러는 향후 Python 버전에서 올바르게 컴파일되거나 실행되지 않을 수 있는 프로그램을 식별하는 데 도움이 되도록 경고를 발행합니다. Python 2.2 또는 nested_scopes
future 문이 사용된 Python 2.1(이 섹션에서는 “future semantics”로 통칭)에서는 컴파일러가 일부 경우에 SyntaxError
를 발행합니다.
경고는 일반적으로 자유 변수를 가진 중첩 함수를 포함하는 함수에 적용됩니다. 예를 들어, 함수 F가 함수 G를 포함하고 G가 빌트인 len()
을 사용하는 경우, F는 자유 변수(len
)를 가진 중첩 함수(G)를 포함하는 함수입니다.
- 함수 스코프에서
import *
사용:import *
가free-in-nested
함수의 본문에서 사용되면 컴파일러는 경고를 발행합니다. 미래 의미론(future semantics) 하에서는SyntaxError
를 발생시킵니다. - 함수 스코프에서 맨
exec
사용:exec
문이 로컬 및 전역 네임스페이스를 지정하는in
키워드 뒤의 두 가지 선택적 표현식을 생략하는 경우를 “맨exec
“라고 합니다. 맨exec
가free-in-nested
함수의 본문에서 사용되면 컴파일러는 경고를 발행합니다. 미래 의미론 하에서는SyntaxError
를 발생시킵니다. - 지역 변수가 전역 변수를 가리는 경우:
free-in-nested
함수가 (1) 중첩 함수에서 사용되고 (2) 전역 변수와 이름이 같은 지역 변수에 대한 바인딩을 가지고 있다면, 컴파일러는 경고를 발행합니다.
둘러싸는 스코프의 이름 재바인딩 (Rebinding names in enclosing scopes)
둘러싸는 스코프의 이름 재바인딩을 지원하는 것은 기술적으로 어렵습니다. 하지만 주된 이유는 귀도 반 로섬(Guido van Rossum)이 이에 반대했기 때문입니다. 그의 동기는, 할당이 둘러싸는 블록의 이름을 재바인딩하도록 프로그래머가 지정할 수 있는 새로운 메커니즘이 필요하며 (예: 키워드 또는 x := 3
와 같은 특수 구문), 이는 지역 변수를 사용하여 클래스 인스턴스에 저장하는 것이 더 나은 상태를 유지하도록 장려할 것이기 때문에 새로운 구문을 추가할 가치가 없다고 보았습니다.
제안된 규칙은 어색하긴 하지만 재바인딩의 효과를 달성할 수 있도록 합니다. 둘러싸는 함수에 의해 효과적으로 재바인딩될 이름은 컨테이너 객체에 바인딩됩니다. 할당 대신 프로그램은 컨테이너의 수정을 사용하여 원하는 효과를 얻습니다.
def bank_account(initial_balance):
balance = [initial_balance] # 리스트를 컨테이너로 사용
def deposit(amount):
balance[0] = balance[0] + amount
return balance
def withdraw(amount):
balance[0] = balance[0] - amount
return balance
return deposit, withdraw
# 예시 사용법
deposit_func, withdraw_func = bank_account(100)
print(deposit_func(50)) # 출력: [150]
print(withdraw_func(20)) # 출력: [130]
중첩 스코프에서 재바인딩을 지원하면 이 코드가 더 명확해질 것입니다. 그러나 deposit()
및 withdraw()
메서드와 balance
를 인스턴스 변수로 정의하는 클래스가 더 명확하므로 클래스가 선호됩니다.
구현 (Implementation)
C Python의 구현은 플랫 클로저(flat closures)를 사용합니다. 함수의 본문 또는 포함된 함수에 자유 변수가 있는 경우, 실행되는 각 def
또는 lambda
표현식은 클로저를 생성합니다. 플랫 클로저를 사용하면 클로저 생성 비용은 다소 비싸지만, 이름 조회(lookup) 비용은 저렴합니다.
이 구현은 여러 새로운 Opcode와 코드 객체에 두 가지 새로운 종류의 이름을 추가합니다. 변수는 특정 코드 객체에 대해 셀 변수(cell variable) 또는 자유 변수(free variable)가 될 수 있습니다. 셀 변수는 포함하는 스코프에 의해 참조됩니다. 결과적으로, 정의된 함수는 호출마다 별도의 저장 공간을 할당해야 합니다. 자유 변수는 함수의 클로저를 통해 참조됩니다.
자유 클로저를 선택한 것은 세 가지 요인에 기반합니다. 첫째, 중첩 함수는 자주 사용되지 않고, 깊이 중첩된(여러 수준의 중첩) 함수는 훨씬 덜 자주 사용된다고 가정합니다. 둘째, 중첩 스코프에서 이름 조회는 빨라야 합니다. 셋째, 중첩 스코프의 사용, 특히 둘러싸는 스코프에 접근하는 함수가 반환되는 경우, 참조되지 않는 객체가 가비지 컬렉터에 의해 회수되는 것을 방해해서는 안 됩니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments