PEP 667 – 네임스페이스에 대한 일관된 뷰
- Author:
- Mark Shannon <mark at hotpy.org>, Tian Gao <gaogaotiantian at hotmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Type:
- Standards Track
- Created:
- 30-Jul-2021
- Python-Version:
- 3.13
- Post-History:
- 20-Aug-2021, 22-Feb-2024
- Resolution:
- 25-Apr-2024
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
초기 버전의 Python에서는 함수, 클래스, 모듈 어디에 있든 모든 네임스페이스가 딕셔너리라는 동일한 방식으로 구현되었습니다.
성능상의 이유로 함수 네임스페이스의 구현이 변경되었습니다. 안타깝게도 이로 인해 locals()와 frame.f_locals를 통해 이러한 네임스페이스에 접근하는 방식은 더 이상 일관되지 않게 되었으며, 스레드, 제너레이터, 코루틴이 추가되면서 수년에 걸쳐 몇 가지 이상한 버그가 스며들었습니다.
이 PEP는 이러한 네임스페이스를 다시 한번 일관되게 만들 것을 제안합니다. frame.f_locals에 대한 수정은 항상 기반 변수에 반영됩니다. 지역 변수에 대한 수정은 즉시 frame.f_locals에 반영되며, 스레딩이나 코루틴과 관계없이 일관성이 유지됩니다.
locals() 함수는 클래스 및 모듈 스코프에서는 현재와 동일하게 동작합니다. 함수 스코프에서는 프레임 객체에 캐시된 하나의 공유 딕셔너리를 암묵적으로 갱신하는 대신, 기반 frame.f_locals의 순간 스냅숏을 반환합니다.
동기
Python 3.12를 포함한 그 이전 릴리스에서 locals()와 frame.f_locals의 구현은 느리고, 일관성이 없으며, 버그가 많습니다. 우리는 이를 더 빠르고 일관되게 만들고, 무엇보다도 버그를 수정하고자 합니다.
예를 들어, 프레임 객체를 통해 지역 변수를 조작하려고 할 때:
class C:
x = 1
sys._getframe().f_locals['x'] = 2
print(x)
2를 출력하지만,:
def f():
x = 1
sys._getframe().f_locals['x'] = 2
print(x)
f()
1을 출력합니다.
이는 일관성이 없고 혼란스럽습니다. 더 나쁜 것은, Python 3.12의 동작이 이상한 버그로 이어질 수 있다는 점입니다.
이 PEP를 적용하면 함수 수준의 변경이 캐시된 딕셔너리 스냅숏이 아니라 프레임의 최적화된 지역 변수에 직접 기록되므로, 두 예제 모두 2를 출력합니다.
Python 3.12의 동작에는 이를 상쇄할 만한 장점이 없습니다. 신뢰할 수 없고 느릴 뿐입니다.
locals() 내장 함수에도 그 나름의 바람직하지 않은 동작이 있습니다. 이러한 우려에 대한 자세한 내용은 PEP 558을 참조하십시오.
근거
frame.f_locals 속성을 write-through 프록시로 만들기
Python 3.12의 frame.f_locals 구현은 지역 변수 배열로부터 즉석에서 생성한 딕셔너리를 반환합니다. 그런 다음 변경 사항을 배열에 다시 기록하려는 디버거와 추적 함수가 PyFrame_LocalsToFast() C API를 호출합니다(Python 3.11까지는 이 API를 추적 함수가 명시적으로 호출하지 않고, 추적 함수가 호출될 때마다 그 뒤에 암묵적으로 호출했습니다).
이로 인해 배열과 딕셔너리가 서로 동기화되지 않을 수 있습니다. PyFrame_LocalsToFast()가 한 번도 호출되지 않으면 f_locals 프레임 속성에 기록한 내용이 지역 변수의 수정으로 반영되지 않을 수 있습니다. 변수가 수정되기 전에 생성된 딕셔너리 스냅숏이 프레임에 다시 기록되면 지역 변수에 기록한 내용이 사라질 수 있습니다(스냅숏을 만든 뒤 프레임에 저장된 값이 바뀌었더라도, 스냅숏에 저장된 알려진 변수 전부가 프레임에 다시 기록되기 때문입니다).
frame.f_locals가 기반 프레임에 대한 뷰를 반환하도록 하면 이러한 문제가 사라집니다. frame.f_locals는 프레임의 복사본이 아니라 뷰이므로 항상 프레임과 동기화됩니다.
locals() 내장 함수가 독립적인 스냅숏을 반환하도록 만들기
PEP 558은 최적화된 스코프에서 locals() 내장 함수의 동작을 표준화하기 위한 세 가지 잠재적 선택지를 검토했습니다:
- 특정 프레임에서
locals()를 호출할 때마다 지역 변수의 공유 스냅숏 하나를 갱신하는 기존 동작을 유지합니다. locals()가 (frame.f_locals와 비슷한) 쓰기 투과(write-through) 프록시 인스턴스를 반환하도록 합니다.locals()가 진정으로 독립적인 스냅숏을 반환하도록 하여,exec()를 통해 지역 변수의 값을 바꾸려는 시도가 어떤 상황에서는 받아들여지는 대신 일관되게 무시되도록 합니다.
언어 레퍼런스에서 가장 쉽게 설명할 수 있고 사용자가 가장 쉽게 기억할 수 있는 방안으로 마지막 방안이 선택되었습니다:
locals()내장 함수는 최적화된 스코프에서는 지역 변수의 순간 스냅숏을 제공하고, 다른 스코프에서는 읽기/쓰기 접근을 제공합니다. 그리고frame.f_locals는 최적화된 스코프를 포함한 모든 스코프에서 지역 변수에 대한 읽기/쓰기 접근을 제공합니다.
이 방식을 사용하면, 쓰기 접근이 필요하지 않거나 원치 않는 경우에도 두 API가 모두 최적화된 스코프에서 완전한 읽기/쓰기 접근을 허용하는 경우보다 코드의 의도가 더 명확해집니다. 이 설계 결정에 대한 자세한 내용은 PEP 558, 특히 동기 절과 최적화된 스코프에서 eval()과 exec()에 대한 추가 고려 사항를 참조하십시오.
이 방식에도 단점이 없는 것은 아니며, 이는 아래의 하위 호환성 절에서 다룹니다.
명세
Python API
frame.f_locals 속성
모듈 스코프와 클래스 스코프(exec() 및 eval() 호출 포함)에서 frame.f_locals는 코드 실행에 사용되는 지역 변수 네임스페이스에 대한 직접 참조입니다.
함수 스코프(및 기타 최적화된 스코프)에서는 기반 프레임의 최적화된 지역 변수 저장 배열과 비지역 변수에 대한 모든 셀 참조의 내용을 직접 수정할 수 있는 새로운 쓰기 투과 프록시 타입의 인스턴스가 됩니다.
뷰 객체는 collections.abc.Mapping 인터페이스를 완전히 구현하며, 다음과 같은 가변 매핑 연산도 구현합니다:
- 대입을 사용한 새 키/값 쌍 추가
- 대입을 사용한 키에 연결된 값 갱신
setdefault()메서드를 통한 조건부 대입update()메서드를 통한 일괄 갱신
서로 다른 프레임의 뷰는 내용이 같더라도 같지 않은 것으로 비교됩니다.
f_locals 매핑에 대한 모든 쓰기는 기반 변수에 즉시 반영됩니다. 기반 변수에 대한 모든 변경은 매핑에 즉시 반영됩니다.
f_locals 객체는 완전한 매핑이며, 임의의 키-값 쌍을 추가할 수 있습니다. 프록시를 통해 추가된 새 이름은 기반 프레임 객체에 저장된 전용 공유 딕셔너리에 저장됩니다(따라서 특정 프레임에 대한 모든 프록시 인스턴스는 이런 방식으로 추가된 어떤 이름에든 접근할 수 있습니다).
(기반 프레임의 지역 변수에 해당하지 않는) 추가 키는 평소처럼 del 문이나 pop() 메서드로 제거할 수 있습니다.
del이나 pop() 메서드를 사용하여 기반 프레임의 지역 변수에 해당하는 키를 제거하는 것은 지원되지 않으며, 그렇게 하려고 하면 ValueError가 발생합니다. 지역 변수는 프록시를 통해 None(또는 다른 값)으로 설정할 수만 있고, 완전히 바인딩 해제할 수는 없습니다.
지역 변수에 해당하는 항목을 삭제할 수 없다는 점을 어떻게 처리해야 할지 불분명하므로, 쓰기 투과 프록시에는 clear() 메서드가 구현되지 않습니다.
하위 호환성을 유지하기 위해, 새 매핑을 만들어야 하는 프록시 API(copy() 등)는 쓰기 투과 프록시 인스턴스가 아닌 일반 내장 dict 인스턴스를 만듭니다.
프레임 객체와 쓰기 투과 프록시 사이에 순환 참조가 생기지 않도록, frame.f_locals에 접근할 때마다 새로운 쓰기 투과 프록시 인스턴스를 반환합니다.
locals() 내장 함수
locals()는 다음과 같이 정의됩니다.:
def locals():
frame = sys._getframe(1)
f_locals = frame.f_locals
if frame._is_optimized(): # Not an actual frame method
f_locals = dict(f_locals)
return f_locals
모듈 스코프와 클래스 스코프(exec() 및 eval() 호출 포함)에서 locals()는 계속해서 코드 실행에 사용되는 지역 변수 네임스페이스에 대한 직접 참조를 반환합니다(이는 frame.f_locals가 보고하는 값과도 같습니다).
최적화된 스코프에서는 locals()를 호출할 때마다 지역 변수의 독립적인 스냅숏이 생성됩니다.
eval() 및 exec() 내장 함수
이 PEP가 locals()의 동작을 변경하므로 eval()과 exec()의 동작도 변경됩니다.
명시적인 네임스페이스 인자를 받아 eval()의 작업을 수행하는 함수 _eval()이 있다고 가정하면, eval()은 다음과 같이 정의할 수 있습니다:
FrameProxyType = type((lambda: sys._getframe().f_locals)())
def eval(expression, /, globals=None, locals=None):
if globals is None:
# No globals -> use calling frame's globals
_calling_frame = sys._getframe(1)
globals = _calling_frame.f_globals
if locals is None:
# No globals or locals -> use calling frame's locals
locals = _calling_frame.f_locals
if isinstance(locals, FrameProxyType):
# Align with locals() builtin in optimized frame
locals = dict(locals)
elif locals is None:
# Globals but no locals -> use same namespace for both
locals = globals
return _eval(expression, globals, locals)
exec()에 대해 명시된 인자 처리도 마찬가지로 갱신됩니다.
(Python 3.12 이하에서는 globals를 함께 제공하지 않고서는 eval()이나 exec()에 locals를 제공할 수 없었는데, 이전에는 이들이 위치 전용 인자였기 때문입니다. 이 PEP와는 별개로, Python 3.13에서는 이 내장 함수들이 키워드 인자를 받도록 갱신되었습니다)
C API
PyEval C API에 추가되는 사항
세 개의 새로운 C-API 함수가 추가됩니다:
PyObject *PyEval_GetFrameLocals(void)
PyObject *PyEval_GetFrameGlobals(void)
PyObject *PyEval_GetFrameBuiltins(void)
PyEval_GetFrameLocals()는 다음과 동등합니다: locals(). PyEval_GetFrameGlobals()는 다음과 동등합니다: globals().
이 함수들은 모두 새 참조를 반환합니다.
PyFrame_GetLocals C API
기존 PyFrame_GetLocals(f) C API는 f.f_locals와 동등합니다. 반환값은 위에서 f.f_locals에 접근하는 경우에 대해 설명한 것과 같습니다.
이 함수는 새 참조를 반환하므로, 최적화된 스코프에서 호출할 때마다 새로운 write-through 프락시 인스턴스를 생성하는 것을 수용할 수 있습니다.
폐지되는 C API
다음 C API 함수들은 빌린 참조를 반환하므로 폐지됩니다:
PyEval_GetLocals()
PyEval_GetGlobals()
PyEval_GetBuiltins()
대신 다음 함수들(새 참조를 반환합니다)을 사용해야 합니다:
PyEval_GetFrameLocals()
PyEval_GetFrameGlobals()
PyEval_GetFrameBuiltins()
다음 C API 함수들은 아무 동작도 하지 않게 되며, 대체 수단 없이 폐지됩니다:
PyFrame_FastToLocalsWithError()
PyFrame_FastToLocals()
PyFrame_LocalsToFast()
폐지되는 모든 함수는 Python 3.13 문서에 폐지됨으로 표시됩니다.
이 함수들 중 상당한 유지 보수 부담을 주는 것은 PyEval_GetLocals()뿐입니다. 따라서 Python 3.14에서 PyEval_GetLocals() 호출은 DeprecationWarning을 발생시키며, 제거 목표 시점은 Python 3.16(Python 3.14 이후 두 번째 릴리스)입니다. PyEval_GetLocals 호환성에 설명된 대로 대안을 사용할 것을 권장합니다.
변경 사항 요약
이 절에서는 Python 3.13 이상에서 명시된 동작이 Python 3.12 이하 버전의 기존 동작과 어떻게 다른지 요약합니다.
Python API 변경 사항
frame.f_locals 변경 사항
다음 예를 살펴보십시오:
def l():
"Get the locals of caller"
return sys._getframe(1).f_locals
def test():
if 0: y = 1 # Make 'y' a local variable
x = 1
l()['x'] = 2
l()['y'] = 4
l()['z'] = 5
y
print(locals(), x)
이 PEP의 변경 사항에 따라 test()는 {'x': 2, 'y': 4, 'z': 5} 2를 출력합니다.
Python 3.12에서는 l()['y'] = 4에 의한 y의 정의가 사라지므로 이 예제는 UnboundLocalError와 함께 실패합니다.
끝에서 두 번째 줄을 y에서 z로 바꾸더라도 Python 3.12에서와 마찬가지로 여전히 NameError가 발생합니다. frame.f_locals에 추가된 키 중 어휘적으로 지역 변수가 아닌 키는 frame.f_locals에서 계속 보이지만, 동적으로 지역 변수가 되지는 않습니다.
locals() 변경 사항
다음 예를 살펴보십시오:
def f():
exec("x = 1")
print(locals().get("x"))
f()
이 PEP의 변경 사항에 따라 이 코드는 (x가 함수에서 정의된 지역 변수인지 여부와 관계없이) 항상 None을 출력합니다. locals()를 명시적으로 호출하면 exec() 호출에서 암묵적으로 사용되는 스냅숏과는 별개의 스냅숏이 만들어지기 때문입니다.
Python 3.12에서는 제시된 예제를 그대로 실행하면 1을 출력하지만, 관련 함수의 정의를 얼핏 무관해 보이게 변경하기만 해도 대신 None을 출력할 수 있습니다(PEP 558의 최적화된 스코프에서 eval()과 exec()에 대한 추가 고려 사항에서 이 주제를 더 자세히 다룹니다).
eval() 및 exec() 변경 사항
eval() 및 exec()에 영향을 주는 주요 변경 사항은 “locals() 변경 사항” 예제에 나와 있습니다. 최적화된 스코프에서 locals()에 반복적으로 접근하더라도 더 이상 공통의 기저 네임스페이스를 암묵적으로 공유하지 않습니다.
C API 변경 사항
PyFrame_GetLocals 변경 사항
exec() 및 eval()은 locals 인자로 임의의 매핑을 받고, 메타클래스는 __prepare__ 메서드에서 임의의 매핑을 반환할 수 있으므로, Python 3.12에서도 이미 PyFrame_GetLocals는 임의의 매핑을 반환할 수 있습니다.
최적화된 스코프에서 프레임 지역 변수 프락시를 반환하는 것은 내장 딕셔너리가 아닌 무언가가 반환되는 경우를 하나 더 추가할 뿐입니다.
PyEval_GetLocals 변경 사항
PyEval_GetLocals()의 의미는 기술적으로는 바뀌지 않지만, 최적화된 프레임에 캐시되는 딕셔너리가 더 이상 프레임 지역 변수에 접근하는 다른 메커니즘(locals() 내장, PyFrame_GetLocals 함수, 프레임 f_locals 속성)과 공유되지 않으므로 실제로는 바뀝니다.
하위 호환성
Python API 호환성
Python 3.12를 포함한 그 이전 버전에서 사용된 구현에는 코너 케이스와 특이한 동작이 많습니다. 이를 우회하는 코드는 변경해야 할 수도 있습니다. 간단한 템플릿 처리나 print 디버깅에 locals()를 사용하는 코드는 계속 올바르게 동작합니다. f_locals를 사용하여 지역 변수를 수정하는 디버거와 기타 도구는 이제 스레드를 사용하는 코드, 코루틴, 제너레이터가 있더라도 올바르게 동작합니다.
frame.f_locals 호환성
f.f_locals는 함수의 네임스페이스인 것처럼 동작하지만, 관찰할 수 있는 차이점이 몇 가지 있습니다. 예를 들어 최적화된 프레임에서는 속성에 접근할 때마다 새로운 write-through 프록시 인스턴스가 생성되므로 f.f_locals is f.f_locals는 False가 됩니다.
하지만 f.f_locals == f.f_locals는 True가 되며, 새 변수 이름을 매핑 키로 추가하는 것을 포함하여 어떤 방법으로든 기저 변수에 가해진 모든 변경 사항은 항상 보입니다.
locals() 호환성
최적화된 프레임에서는 locals() is locals()가 False가 되므로, 다음과 같은 코드는 1을 반환하는 대신 KeyError를 발생시킵니다:
def f():
locals()["x"] = 1
return locals()["x"]
이러한 코드가 계속 동작하려면, 프레임 객체에 대한 기존의 암묵적 캐싱에 의존하는 대신 수정할 네임스페이스를 지역 변수에 명시적으로 저장해야 합니다:
def f():
ns = {}
ns["x"] = 1
return ns["x"]
엄밀히 말해 이는 공식적인 하위 호환성 파괴는 아니지만(locals()에 다시 쓰는 동작은 정의되지 않은 것으로 명시적으로 문서화되어 있었기 때문입니다), 기존 동작에 의존하는 코드는 분명히 존재합니다. 따라서 바뀐 동작은 문서에 변경 사항으로 명시되며, Python 3.13 이식 가이드에서도 다룹니다.
최적화된 스코프에서 Python 3.13+에서는 불필요한 복사본을 만들지 않으면서 모든 버전에서 locals()의 복사본으로 작업하려면, 사용자는 Python 3.13 이전의 Python 버전에서만 명시적으로 복사본을 만드는 버전 의존적인 헬퍼 함수를 정의해야 합니다.:
if sys.version_info >= (3, 13):
def _ensure_func_snapshot(d):
return d # 3.13+ locals() already returns a snapshot
else:
def _ensure_func_snapshot(d):
return dict(d) # Create snapshot on older versions
def f():
ns = _ensure_func_snapshot(locals())
ns["x"] = 1
return ns
다른 스코프에서는 불필요한 복사 없이 locals().copy()를 조건 없이 계속 호출할 수 있습니다.
exec() 및 eval()에 미치는 영향
이 PEP가 exec()이나 eval()을 직접 수정하지는 않지만, locals()의 의미 변경은 exec()과 eval()의 동작에 영향을 줍니다. 이 함수들은 기본적으로 호출한 쪽의 네임스페이스에서 코드를 실행하기 때문입니다.
이는 일부 코드에 잠재적인 호환성 문제를 일으킵니다. 함수 스코프에서 locals()를 여러 번 호출할 때 같은 딕셔너리를 반환하던 이전 구현에서는 지역 변수 네임스페이스가 암묵적으로 공유되었기 때문에 다음 코드가 대개 동작했습니다.:
def f():
exec('a = 0') # equivalent to exec('a = 0', globals(), locals())
exec('print(a)') # equivalent to exec('print(a)', globals(), locals())
print(locals()) # {'a': 0}
# However, print(a) will not work here
f()
이 PEP에서 locals()의 의미가 바뀌면 exec('print(a)')' 호출은 NameError로 실패하고, print(locals())는 빈 딕셔너리를 보고합니다. 각 줄이 프레임 객체에 저장된 하나의 캐시된 스냅숏을 암묵적으로 공유하는 대신, 지역 변수에 대한 각자의 독립적인 스냅숏을 사용하기 때문입니다.
이전처럼 암묵적으로 공유되던 프레임 네임스페이스에 의존하는 대신 명시적인 네임스페이스를 사용하면, 여러 exec() 호출에 걸쳐 공유되는 네임스페이스를 여전히 얻을 수 있습니다.:
def f():
ns = {}
exec('a = 0', locals=ns)
exec('print(a)', locals=ns) # 0
f()
frame.f_locals를 명시적으로 사용하면 지역 스코프의 변수를 안정적으로 변경할 수도 있는데, 이는 이전에는 불가능했던 일입니다(ctypes를 사용해 PyFrame_LocalsToFast를 호출하더라도 이 PEP의 다른 부분에서 다루는 상태 불일치 문제를 피할 수 없었습니다).:
def f():
a = None
exec('a = 0', locals=sys._getframe().f_locals)
print(a) # 0
f()
모듈 및 클래스 스코프(중첩된 호출 포함)에서의 exec()과 eval()의 동작은 바뀌지 않습니다. 해당 스코프에서는 locals()의 동작이 바뀌지 않기 때문입니다.
표준 라이브러리의 다른 코드 실행 API에 미치는 영향
pdb와 bdb는 frame.f_locals API를 사용하므로, 최적화된 프레임에서도 지역 변수를 안정적으로 갱신할 수 있게 됩니다. 이 PEP를 구현하면 디버거가 활성화된 동안 동시 코드 실행을 허용하는 스레드, 제너레이터, 코루틴 및 기타 메커니즘과 관련하여 이 모듈들에 오랫동안 남아 있던 여러 버그가 해결됩니다.
표준 라이브러리의 다른 코드 실행 API(예: code 모듈)는 locals() 또는 frame.f_locals에 암묵적으로 접근하지 않지만, 이 네임스페이스들을 명시적으로 전달할 때의 동작은 이 PEP의 나머지 부분에서 설명하는 대로 바뀝니다(최적화된 스코프에서 locals()를 전달하면 더 이상 여러 호출에 걸쳐 코드 실행 네임스페이스를 암묵적으로 공유하지 않으며, 최적화된 스코프에서 frame.f_locals를 전달하면 지역 변수와 비지역 셀 참조를 안정적으로 수정할 수 있습니다).
C API 호환성
PyEval_GetLocals 호환성
PyEval_GetLocals()는 역사적으로 Python 수준에서 locals()를 흉내 내는 것인지 sys._getframe().f_locals를 흉내 내는 것인지 구별한 적이 없습니다. 이들 모두 지역 변수 바인딩에 대한 동일한 공유 캐시의 참조를 반환했기 때문입니다.
이 PEP에 따라 locals()는 최적화된 프레임에서 호출할 때마다 독립적인 스냅숏을 반환하도록 바뀌고, frame.f_locals는 (PyFrame_GetLocals와 마찬가지로) 새로운 라이트스루(write-through) 프록시 인스턴스를 반환하도록 바뀝니다.
PyEval_GetLocals()는 빌린 참조(borrowed reference)를 반환하므로 그 의미를 두 대안 중 어느 쪽에 맞추어 갱신하는 것도 불가능하며, 그 결과 프레임 객체에 저장된 공유 캐시 딕셔너리를 필요로 하는 유일한 API로 남게 됩니다.
기술적으로는 이 함수의 의미가 바뀌지 않지만, 다른 API들이 더 이상 동일한 하부 캐시 딕셔너리에 접근하지 않으므로 추가된 딕셔너리 항목을 다른 API 사용자에게 보이게 할 수는 없게 됩니다.
PyEval_GetLocals()를 Python locals() 내장과 동등한 것으로 사용하고 있다면, 대신 PyEval_GetFrameLocals()를 사용해야 합니다.
다음 코드는:
locals = PyEval_GetLocals();
if (locals == NULL) {
goto error_handler;
}
Py_INCREF(locals);
다음과 같이 대체해야 합니다.:
// Equivalent to "locals()" in Python code
locals = PyEval_GetFrameLocals();
if (locals == NULL) {
goto error_handler;
}
PyEval_GetLocals()를 Python에서 sys._getframe().f_locals를 호출하는 것과 동등한 것으로 사용하고 있다면, PyEval_GetFrame()의 결과에 대해 PyFrame_GetLocals()를 호출하는 것으로 대체해야 합니다.
이러한 경우 원래 코드는 다음과 같이 대체해야 합니다.:
// Equivalent to "sys._getframe()" in Python code
frame = PyEval_GetFrame();
if (frame == NULL) {
goto error_handler;
}
// Equivalent to "frame.f_locals" in Python code
locals = PyFrame_GetLocals(frame);
frame = NULL; // Minimise visibility of borrowed reference
if (locals == NULL) {
goto error_handler;
}
PEP 709 인라인 컴프리헨션에 미치는 영향
함수 내의 인라인 컴프리헨션의 경우, 현재 locals()는 컴프리헨션 안팎에서 동일하게 동작하며 이는 바뀌지 않습니다. 함수 내부에서의 locals() 동작은 대체로 이 PEP의 나머지 부분에서 명시한 대로 바뀝니다.
모듈 또는 클래스 스코프의 인라인 컴프리헨션의 경우, 인라인 컴프리헨션 안에서 locals()를 호출하면 호출할 때마다 새 딕셔너리를 반환합니다. 이 PEP는 함수 내의 locals()도 호출할 때마다 항상 새 딕셔너리를 반환하게 하여 일관성을 높입니다. 클래스 또는 모듈 스코프의 인라인 컴프리헨션은 마치 인라인 컴프리헨션이 여전히 별개의 함수인 것처럼 동작하는 것으로 보일 것입니다.
구현
frame.f_locals를 읽을 때마다, 지역 변수(셀 변수와 자유 변수 포함)의 이름을 해당 지역 변수의 값에 대응시키는 매핑처럼 보이는 새 프록시 객체가 생성됩니다.
가능한 구현의 개요는 아래와 같습니다. 밑줄로 시작하는 모든 속성은 보이지 않으며 직접 접근할 수 없습니다. 이 속성들은 제안된 설계를 설명하기 위한 용도로만 사용됩니다.
NULL: Object # NULL is a singleton representing the absence of a value.
class CodeType:
_name_to_offset_mapping_impl: dict | NULL
_cells: frozenset # Set of indexes of cell and free variables
...
def __init__(self, ...):
self._name_to_offset_mapping_impl = NULL
self._variable_names = deduplicate(
self.co_varnames + self.co_cellvars + self.co_freevars
)
...
@property
def _name_to_offset_mapping(self):
"Mapping of names to offsets in local variable array."
if self._name_to_offset_mapping_impl is NULL:
self._name_to_offset_mapping_impl = {
name: index for (index, name) in enumerate(self._variable_names)
}
return self._name_to_offset_mapping_impl
class FrameType:
_locals : array[Object] # The values of the local variables, items may be NULL.
_extra_locals: dict | NULL # Dictionary for storing extra locals not in _locals.
_locals_cache: FrameLocalsProxy | NULL # required to support PyEval_GetLocals()
def __init__(self, ...):
self._extra_locals = NULL
self._locals_cache = NULL
...
@property
def f_locals(self):
return FrameLocalsProxy(self)
class FrameLocalsProxy:
"Implements collections.MutableMapping."
__slots__ = ("_frame", )
def __init__(self, frame:FrameType):
self._frame = frame
def __getitem__(self, name):
f = self._frame
co = f.f_code
if name in co._name_to_offset_mapping:
index = co._name_to_offset_mapping[name]
val = f._locals[index]
if val is NULL:
raise KeyError(name)
if index in co._cells
val = val.cell_contents
if val is NULL:
raise KeyError(name)
return val
else:
if f._extra_locals is NULL:
raise KeyError(name)
return f._extra_locals[name]
def __setitem__(self, name, value):
f = self._frame
co = f.f_code
if name in co._name_to_offset_mapping:
index = co._name_to_offset_mapping[name]
kind = co._local_kinds[index]
if index in co._cells
cell = f._locals[index]
cell.cell_contents = val
else:
f._locals[index] = val
else:
if f._extra_locals is NULL:
f._extra_locals = {}
f._extra_locals[name] = val
def __iter__(self):
f = self._frame
co = f.f_code
yield from iter(f._extra_locals)
for index, name in enumerate(co._variable_names):
val = f._locals[index]
if val is NULL:
continue
if index in co._cells:
val = val.cell_contents
if val is NULL:
continue
yield name
def __contains__(self, item):
f = self._frame
if item in f._extra_locals:
return True
return item in co._variable_names
def __len__(self):
f = self._frame
co = f.f_code
res = 0
for index, _ in enumerate(co._variable_names):
val = f._locals[index]
if val is NULL:
continue
if index in co._cells:
if val.cell_contents is NULL:
continue
res += 1
return len(self._extra_locals) + res
C API
PyEval_GetLocals()는 대략 다음과 같이 구현됩니다.:
PyObject *PyEval_GetLocals(void) {
PyFrameObject * = ...; // Get the current frame.
if (frame->_locals_cache == NULL) {
frame->_locals_cache = PyEval_GetFrameLocals();
} else {
PyDict_Update(frame->_locals_cache, PyFrame_GetLocals(frame));
}
return frame->_locals_cache;
}
빌린 참조(borrowed reference)를 반환하는 모든 함수와 마찬가지로, 객체의 수명이 끝난 뒤에 그 참조를 사용하지 않도록 주의해야 합니다.
구현 참고 사항
채택 당시 PEP 본문은 PyEval_GetLocals가 새 write-through 프록시의 캐시된 인스턴스를 반환하게 된다고 제시한 반면, 구현 스케치는 프레임 인스턴스에 캐시된 딕셔너리 스냅숏을 계속 반환한다고 나타냈습니다. 이 불일치는 PEP를 구현하는 과정에서 발견되었으며, 프레임 인스턴스에 캐시된 딕셔너리 스냅숏을 반환하는 Python 3.12의 동작을 유지하는 방향으로 운영 위원회(Steering Council)가 해결했습니다. PEP 본문도 이에 맞게 갱신되었습니다.
C API를 명확히 하는 논의 과정에서, 최적화된 스코프에서 locals()가 독립적인 스냅숏을 반환하도록 변경된 근거가 명확하지 않다는 점도 드러났습니다. 이 근거는 이 PEP에서 독자적으로 다루어진 것이 아니라 원래의 PEP 558 논의에서 이어받은 것이었기 때문입니다. 이 변경 사항을 더 잘 설명하도록 PEP 본문을 갱신했으며, 기본적으로 locals() 네임스페이스에서 코드를 실행하는 코드 실행 API에 미치는 영향을 다루도록 명세 및 하위 호환성 절도 추가로 갱신했습니다. PEP 558에도 동기와 근거에 관한 세부 내용이 추가되었습니다.
3.13.0에서는 write-through 프록시가 추가 변수조차 del과 pop()으로 삭제하는 것을 허용하지 않았습니다. 이는 이후 호환성 회귀로 보고되었으며, 현재 frame.f_locals 속성에 설명된 대로 해결되었습니다.
PEP 558과의 비교
이 PEP와 PEP 558은 locals()와 frame.f_locals()의 의미를 이해하기 쉽게 만들고 그 동작을 신뢰할 수 있게 한다는 공통의 목표를 가지고 있었습니다.
이 PEP와 PEP 558의 핵심적인 차이는, PEP 558이 레거시 PyEval_GetLocals() API와의 하위 호환성을 개선하기 위해 지역 변수 전체를 복사한 내부 딕셔너리 안에 추가 변수를 저장하려 한 반면, 이 PEP는 그렇게 하지 않는다는 점입니다(이 PEP는 추가 지역 변수를 새 프레임 프록시 객체를 통해서만 접근하는 전용 딕셔너리에 저장하고, 요청이 있을 때에만 PyEval_GetLocals() 공유 딕셔너리로 복사합니다).
PEP 558은 그 내부 사본이 정확히 언제 갱신되는지 명시하지 않았기 때문에, 이 PEP에서는 명확하게 규정되는 여러 경우에 PEP 558의 동작은 추론할 수 없었습니다.
PEP 558은 또한 C API에 몇 가지 Python 스코프 인트로스펙션 인터페이스를 추가하자고 제안했습니다. 이 인터페이스를 사용하면 확장 모듈은 현재 활성화된 Python 스코프가 최적화되어 있는지, 따라서 C API의 locals() 대응 함수가 프레임의 지역 실행 네임스페이스에 대한 직접 참조를 반환하는지 아니면 프레임의 지역 변수와 nonlocal 셀 참조의 얕은 복사본을 반환하는지를 더 쉽게 판단할 수 있습니다. 이러한 인트로스펙션 API를 추가할지 여부는 locals()와 frame.f_locals에 대해 제안된 변경 사항과는 별개의 문제이므로, 이 PEP에는 그러한 제안을 포함하지 않았습니다.
PEP 558은 이 PEP를 채택하는 쪽으로 최종적으로 철회되었습니다.
참조 구현
구현은 GitHub의 초안 풀 리퀘스트로 개발 중입니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.