[Final] PEP 667 - Consistent views of namespaces
원문 링크: PEP 667 - Consistent views of namespaces
상태: Final 유형: Standards Track 작성일: 30-Jul-2021
PEP 667 – Consistent views of namespaces (네임스페이스의 일관된 뷰)
- 작성자: Mark Shannon, Tian Gao
- 상태: Final (최종)
- 유형: Standards Track
- Python 버전: 3.13
- 생성일: 2021년 7월 30일
- 해결일: 2024년 4월 25일
이 PEP는 역사적인 문서이며, 최신 및 공식 문서는 locals()에서 확인할 수 있습니다.
개요 (Abstract)
초기 Python 버전에서는 함수, 클래스, 모듈 등 모든 네임스페이스가 딕셔너리(dictionary)와 동일한 방식으로 구현되었습니다. 그러나 성능상의 이유로 함수 네임스페이스의 구현이 변경되었고, 이로 인해 locals()
및 frame.f_locals
를 통한 네임스페이스 접근 방식이 일관성을 잃게 되었습니다. 시간이 지남에 따라 스레드, 제너레이터(generator), 코루틴(coroutine)이 추가되면서 일부 예기치 않은 버그가 발생했습니다.
이 PEP는 이러한 네임스페이스를 다시 일관성 있게 만드는 것을 제안합니다. frame.f_locals
에 대한 수정은 항상 기본 변수에서 즉시 확인할 수 있습니다. 지역 변수(local variables)에 대한 수정은 frame.f_locals
에서 즉시 확인할 수 있으며, 스레딩(threading)이나 코루틴(coroutines) 여부와 관계없이 일관성을 유지합니다.
locals()
함수는 클래스(class) 및 모듈(module) 스코프(scope)에서는 현재와 동일하게 작동합니다. 함수 스코프에서는 프레임(frame) 객체에 캐시(cache)된 단일 공유 딕셔너리를 암시적으로 새로 고치는 대신, 기본 frame.f_locals
의 즉각적인 스냅샷(snapshot)을 반환합니다.
동기 (Motivation)
Python 3.12 이전 버전의 locals()
와 frame.f_locals
구현은 느리고, 일관성이 없으며, 버그가 많았습니다. 이 PEP는 이를 더 빠르고, 일관성 있게 만들고, 가장 중요하게는 버그를 수정하는 것을 목표로 합니다.
예를 들어, frame
객체를 통해 지역 변수를 조작하려 할 때:
class C:
x = 1
import sys
sys._getframe().f_locals['x'] = 2
print(x) # 2를 출력
위 코드는 2
를 출력하지만, 함수에서는 다음과 같이 동작했습니다.
def f():
x = 1
import sys
sys._getframe().f_locals['x'] = 2
print(x)
f() # 1을 출력
이러한 불일치는 혼란스러웠습니다. Python 3.12의 동작은 이상한 버그를 유발할 수 있었습니다.
이 PEP를 통해 두 예제 모두 2
를 출력하게 됩니다. 함수 수준의 변경 사항이 캐시된 딕셔너리 스냅샷이 아닌 프레임의 최적화된 지역 변수에 직접 기록되기 때문입니다.
Python 3.12의 동작에는 보상할 만한 이점이 없었으며, 신뢰할 수 없고 느렸습니다. locals()
내장 함수(builtin)도 바람직하지 않은 동작을 가지고 있었으며, 이에 대한 자세한 내용은 PEP 558에 명시되어 있습니다.
근거 (Rationale)
frame.f_locals
속성을 Write-Through Proxy로 만들기 (Making the frame.f_locals attribute a write-through proxy)
Python 3.12의 frame.f_locals
구현은 지역 변수 배열에서 즉석에서 생성된 딕셔너리를 반환했습니다. 디버거(debugger)와 추적 함수(trace functions)가 변경 사항을 다시 배열에 쓰기 위해 PyFrame_LocalsToFast()
C API를 호출했습니다. (Python 3.11까지는 이 API가 모든 추적 함수 호출 후에 암시적으로 호출되었지만, 이후에는 추적 함수에서 명시적으로 호출해야 했습니다.)
이로 인해 배열과 딕셔너리가 서로 동기화되지 않는 문제가 발생할 수 있었습니다. PyFrame_LocalsToFast()
가 호출되지 않으면 f_locals
프레임 속성에 대한 쓰기가 지역 변수 수정으로 나타나지 않을 수 있습니다. 변수가 수정되기 전에 생성된 딕셔너리 스냅샷이 프레임에 다시 기록되면 지역 변수에 대한 쓰기가 손실될 수 있습니다.
frame.f_locals
가 기본 프레임에 대한 뷰(view)를 반환하도록 함으로써 이러한 문제가 사라집니다. frame.f_locals
는 복사본이 아닌 뷰이기 때문에 항상 프레임과 동기화됩니다.
locals()
내장 함수가 독립적인 스냅샷을 반환하도록 만들기 (Making the locals() builtin return independent snapshots)
PEP 558은 최적화된 스코프에서 locals()
내장 함수의 동작을 표준화하기 위해 세 가지 잠재적인 옵션을 고려했습니다.
- 주어진 프레임에 대한
locals()
호출마다 지역 변수의 단일 공유 스냅샷을 업데이트하는 기존 동작을 유지합니다. locals()
가 write-through proxy 인스턴스(frame.f_locals
와 유사)를 반환하도록 합니다.locals()
가 완전히 독립적인 스냅샷을 반환하도록 하여,exec()
를 통해 지역 변수의 값을 변경하려는 시도가 특정 상황에서 수용되는 대신 일관되게 무시되도록 합니다.
마지막 옵션이 언어 레퍼런스(language reference)에서 가장 쉽게 설명되고 사용자가 기억하기 쉽다는 이유로 선택되었습니다.
locals()
내장 함수는 최적화된 스코프에서 지역 변수의 즉각적인 스냅샷을 제공하며, 다른 스코프에서는 읽기/쓰기 접근을 제공합니다.frame.f_locals
는 최적화된 스코프를 포함한 모든 스코프에서 지역 변수에 대한 읽기/쓰기 접근을 제공합니다.
이 접근 방식은 쓰기 접근이 필요하지 않거나 바람직하지 않은 경우에도 두 API가 최적화된 스코프에서 전체 읽기/쓰기 접근을 허용하는 것보다 코드의 의도를 더 명확하게 할 수 있습니다. 이 설계 결정에 대한 자세한 내용은 PEP 558, 특히 ‘Motivation’ 섹션과 ‘Additional considerations for eval() and exec() in optimized scopes’를 참조하십시오.
이 접근 방식에도 단점이 있으며, 이는 아래의 ‘하위 호환성(Backwards Compatibility)’ 섹션에서 다룹니다.
명세 (Specification)
Python API
frame.f_locals
속성 (The frame.f_locals attribute)
모듈(module) 및 클래스(class) 스코프(exec()
및 eval()
호출 포함)의 경우, frame.f_locals
는 코드 실행에 사용되는 지역 변수 네임스페이스에 대한 직접적인 참조입니다.
함수 스코프(및 기타 최적화된 스코프)의 경우, frame.f_locals
는 새로운 write-through proxy 타입의 인스턴스가 됩니다. 이 프록시는 기본 프레임의 최적화된 지역 변수 저장 배열과 비지역 변수에 대한 셀(cell) 참조 내용을 직접 수정할 수 있습니다.
뷰(view) 객체는 collections.abc.Mapping
인터페이스를 완전히 구현하며, 다음과 같은 변경 가능한 매핑(mutable mapping) 작업을 구현합니다.
- 할당을 사용하여 새로운 키-값 쌍 추가
- 할당을 사용하여 키와 연결된 값 업데이트
setdefault()
메서드를 통한 조건부 할당update()
메서드를 통한 대량 업데이트
내용이 동일하더라도 다른 프레임의 뷰는 같지 않다고 비교됩니다.
f_locals
매핑에 대한 모든 쓰기는 기본 변수에서 즉시 확인할 수 있습니다. 기본 변수에 대한 모든 변경은 매핑에서 즉시 확인할 수 있습니다.
f_locals
객체는 완전한 매핑이며, 임의의 키-값 쌍을 추가할 수 있습니다. 프록시를 통해 추가된 새로운 이름은 기본 프레임 객체에 저장된 전용 공유 딕셔너리에 저장됩니다. (따라서 주어진 프레임에 대한 모든 프록시 인스턴스는 이런 방식으로 추가된 모든 이름에 접근할 수 있습니다.)
del
문이나 pop()
메서드를 사용하여 기본 프레임의 지역 변수에 해당하지 않는 추가 키를 제거할 수 있습니다.
del
또는 pop()
메서드를 사용하여 기본 프레임의 지역 변수에 해당하는 키를 제거하는 것은 지원되지 않으며, 그렇게 시도하면 ValueError
가 발생합니다. 지역 변수는 프록시를 통해 None
(또는 다른 값)으로만 설정할 수 있으며, 완전히 언바인딩(unbound)할 수 없습니다.
clear()
메서드는 write-through proxy에서 구현되지 않습니다. 이는 지역 변수에 해당하는 항목을 삭제할 수 없는 경우 어떻게 처리해야 할지 불분명하기 때문입니다.
하위 호환성(backwards compatibility)을 유지하기 위해, copy()
와 같이 새로운 매핑을 생성해야 하는 프록시 API는 write-through proxy 인스턴스 대신 일반적인 내장 dict
인스턴스를 생성합니다.
프레임 객체와 write-through proxy 사이에 순환 참조(circular reference)가 발생하는 것을 피하기 위해, frame.f_locals
에 대한 각 접근은 새로운 write-through proxy 인스턴스를 반환합니다.
locals()
내장 함수 (The locals() builtin)
locals()
는 다음과 같이 정의됩니다.
def locals():
frame = sys._getframe(1)
f_locals = frame.f_locals
if frame._is_optimized(): # 실제 프레임 메서드는 아님
f_locals = dict(f_locals)
return f_locals
모듈(module) 및 클래스(class) 스코프(exec()
및 eval()
호출 포함)의 경우, locals()
는 코드 실행에 사용되는 지역 변수 네임스페이스에 대한 직접적인 참조를 계속 반환합니다. (frame.f_locals
에서 보고되는 값과 동일합니다.)
최적화된 스코프에서는 locals()
에 대한 각 호출이 지역 변수의 독립적인 스냅샷을 생성합니다.
eval()
및 exec()
내장 함수 (The eval() and exec() builtins)
이 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:
# globals가 없으면 -> 호출 프레임의 globals 사용
_calling_frame = sys._getframe(1)
globals = _calling_frame.f_globals
if locals is None:
# globals 또는 locals가 없으면 -> 호출 프레임의 locals 사용
locals = _calling_frame.f_locals
if isinstance(locals, FrameProxyType):
# 최적화된 프레임에서 locals() 내장 함수와 정렬
locals = dict(locals)
elif locals is None:
# globals는 있지만 locals는 없으면 -> 둘 다 동일한 네임스페이스 사용
locals = globals
return _eval(expression, globals, locals)
exec()
에 대한 지정된 인수 처리도 유사하게 업데이트됩니다.
(Python 3.12 및 이전 버전에서는 eval()
또는 exec()
에 globals
를 제공하지 않고 locals
를 제공하는 것이 불가능했습니다. 이는 이전에 위치 전용 인수였기 때문입니다. 이 PEP와는 별개로, Python 3.13은 이 내장 함수들이 키워드 인수를 받도록 업데이트되었습니다.)
C API
PyEval C API에 추가 (Additions to the PyEval C API)
세 가지 새로운 C-API 함수가 추가됩니다.
PyObject *PyEval_GetFrameLocals(void)
PyObject *PyEval_GetFrameGlobals(void)
PyObject *PyEval_GetFrameBuiltins(void)
PyEval_GetFrameLocals()
는 Python의 locals()
와 동일합니다. PyEval_GetFrameGlobals()
는 Python의 globals()
와 동일합니다. 이 모든 함수는 새로운 참조(new reference)를 반환합니다.
PyFrame_GetLocals
C API
기존 PyFrame_GetLocals(f)
C API는 Python의 f.f_locals
와 동일합니다. 반환 값은 f.f_locals
접근에 대해 위에서 설명한 것과 같습니다.
이 함수는 새로운 참조를 반환하므로, 최적화된 스코프에서 각 호출 시 새로운 write-through proxy 인스턴스를 생성할 수 있습니다.
더 이상 사용되지 않는 C API (Deprecated C APIs)
다음 C API 함수는 빌려온 참조(borrowed references)를 반환하므로 더 이상 사용되지 않습니다.
PyEval_GetLocals()
PyEval_GetGlobals()
PyEval_GetBuiltins()
대신 다음 함수(새로운 참조를 반환하는)를 사용해야 합니다.
PyEval_GetFrameLocals()
PyEval_GetFrameGlobals()
PyEval_GetFrameBuiltins()
다음 C API 함수는 아무런 동작도 하지 않으며, 대체 없이 더 이상 사용되지 않습니다.
PyFrame_FastToLocalsWithError()
PyFrame_FastToLocals()
PyFrame_LocalsToFast()
더 이상 사용되지 않는 모든 함수는 Python 3.13 문서에 deprecated로 표시됩니다.
이러한 함수 중 PyEval_GetLocals()
만이 상당한 유지보수 부담을 가집니다. 따라서 PyEval_GetLocals()
호출은 Python 3.14에서 DeprecationWarning
을 발생시키고, Python 3.16(Python 3.14 이후 두 릴리스)에 제거될 예정입니다. 대안은 PyEval_GetLocals
호환성에서 설명된 대로 권장됩니다.
변경 사항 요약 (Summary of Changes)
이 섹션은 Python 3.13 이후 버전에서 지정된 동작이 Python 3.12 및 이전 버전의 기존 동작과 어떻게 다른지 요약합니다.
Python API 변경 사항 (Python API changes)
frame.f_locals
변경 사항 (frame.f_locals
changes)
다음 예시를 고려해 봅시다.
import sys
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
print(locals(), x)
test()
이 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
에 추가되었지만 어휘적으로(lexically) 지역 변수가 아닌 키는 frame.f_locals
에는 계속 표시되지만, 동적으로 지역 변수가 되지는 않습니다.
locals()
변경 사항 (locals()
changes)
다음 예시를 고려해 봅시다.
def f():
exec("x = 1")
print(locals().get("x"))
f()
이 PEP의 변경 사항을 적용하면, 이 코드는 항상 None
을 출력합니다. (x
가 함수에 정의된 지역 변수인지 여부와 관계없이) 이는 locals()
에 대한 명시적 호출이 exec()
호출에서 암시적으로 사용되는 것과 다른 별개의 스냅샷을 생성하기 때문입니다.
Python 3.12에서는 위 예시가 1
을 출력했지만, 함수 정의에 대한 겉보기에는 관련 없는 변경으로 인해 None
을 출력할 수도 있었습니다. (PEP 558의 ‘Additional considerations for eval() and exec() in exec() and eval() in optimized scopes’ 섹션에 이 주제에 대한 자세한 내용이 있습니다.)
eval()
및 exec()
변경 사항 (eval()
and exec()
changes)
eval()
및 exec()
에 영향을 미치는 주요 변경 사항은 “locals() 변경 사항” 예시에서 보여집니다. 최적화된 스코프에서 locals()
에 반복적으로 접근하는 것이 더 이상 암시적으로 공통의 기본 네임스페이스를 공유하지 않게 됩니다.
C API 변경 사항 (C API changes)
PyFrame_GetLocals
변경 사항 (PyFrame_GetLocals
change)
PyFrame_GetLocals
는 이미 Python 3.12에서 임의의 매핑을 반환할 수 있었습니다. exec()
및 eval()
은 locals
인수로 임의의 매핑을 허용하며, 메타클래스(metaclass)는 __prepare__
메서드에서 임의의 매핑을 반환할 수 있습니다.
최적화된 스코프에서 프레임 로컬 프록시(frame locals proxy)를 반환하는 것은 내장 딕셔너리가 아닌 다른 것이 반환되는 또 다른 경우를 추가할 뿐입니다.
PyEval_GetLocals
변경 사항 (PyEval_GetLocals
change)
PyEval_GetLocals()
의 의미론(semantics)은 기술적으로 변경되지 않았지만, 실제로는 변경됩니다. 최적화된 프레임에 캐시된 딕셔너리가 더 이상 프레임 로컬에 접근하는 다른 메커니즘( locals()
내장 함수, PyFrame_GetLocals
함수, frame.f_locals
속성)과 공유되지 않기 때문입니다.
하위 호환성 (Backwards Compatibility)
Python API 호환성 (Python API compatibility)
Python 3.12 이하 버전에서 사용된 구현은 많은 예외적인 경우(corner cases)와 특이한 점이 있었습니다. 이러한 문제를 해결하기 위해 작성된 코드는 변경해야 할 수도 있습니다. 단순한 템플릿(templating)이나 print
디버깅을 위해 locals()
를 사용하는 코드는 계속해서 올바르게 작동할 것입니다. 디버거와 f_locals
를 사용하여 지역 변수를 수정하는 다른 도구들은 이제 스레드 코드, 코루틴, 제너레이터와 같은 동시성(concurrent) 코드 실행 메커니즘이 있는 경우에도 올바르게 작동할 것입니다.
frame.f_locals
호환성 (frame.f_locals
compatibility)
f.f_locals
는 함수의 네임스페이스인 것처럼 동작하지만, 몇 가지 눈에 띄는 차이점이 있습니다. 예를 들어, 최적화된 프레임의 경우 f.f_locals is f.f_locals
는 False
가 됩니다. 이는 속성에 접근할 때마다 새로운 write-through proxy 인스턴스가 생성되기 때문입니다.
그러나 f.f_locals == f.f_locals
는 True
이며, 매핑 키로 새 변수 이름을 추가하는 것을 포함하여 어떤 방식으로든 기본 변수에 대한 모든 변경은 항상 확인할 수 있습니다.
locals()
호환성 (locals()
compatibility)
최적화된 프레임의 경우 locals() is locals()
는 False
이므로, 다음 코드와 같은 코드는 1
을 반환하는 대신 KeyError
를 발생시킵니다.
def f():
locals()["x"] = 1
return locals()["x"]
이러한 코드가 계속 작동하려면, 이전의 암시적인 프레임 객체 캐싱에 의존하는 대신 수정할 네임스페이스를 지역 변수에 명시적으로 저장해야 합니다.
def f():
ns = {}
ns["x"] = 1
return ns["x"]
이것은 기술적으로 공식적인 하위 호환성 파기(formal backwards compatibility break)는 아닙니다. (locals()
에 다시 쓰는 동작이 명시적으로 정의되지 않은 것으로 문서화되었기 때문입니다.) 하지만 기존 동작에 의존하는 코드가 분명히 존재합니다. 따라서 업데이트된 동작은 문서에 변경 사항으로 명시적으로 기록될 것이며, Python 3.13 포팅 가이드(porting guide)에서 다룰 예정입니다.
Python 3.13 이상에서 중복 복사본을 만들지 않고 모든 버전의 최적화된 스코프에서 locals()
의 복사본으로 작업하려면, 사용자는 Python 3.13 이전 버전에서만 명시적으로 복사본을 만드는 버전 종속 헬퍼 함수를 정의해야 합니다.
import sys
if sys.version_info >= (3, 13):
def _ensure_func_snapshot(d):
return d # 3.13+ locals()는 이미 스냅샷을 반환함
else:
def _ensure_func_snapshot(d):
return dict(d) # 이전 버전에서는 스냅샷 생성
def f():
ns = _ensure_func_snapshot(locals())
ns["x"] = 1
return ns
다른 스코프에서는 locals().copy()
를 무조건 호출해도 중복 복사본이 생성되지 않습니다.
exec()
및 eval()
에 미치는 영향 (Impact on exec() and eval())
이 PEP가 exec()
또는 eval()
을 직접 수정하지는 않지만, locals()
에 대한 의미론적 변경은 exec()
및 eval()
의 동작에 영향을 미칩니다. 이는 기본적으로 호출하는 네임스페이스에서 코드를 실행하기 때문입니다.
이는 일부 코드에 잠재적인 호환성 문제를 제기합니다. 이전 구현에서는 함수 스코프에서 locals()
가 여러 번 호출될 때 동일한 딕셔너리를 반환했기 때문에, 다음 코드는 암시적으로 공유된 지역 변수 네임스페이스 덕분에 일반적으로 작동했습니다.
def f():
exec('a = 0') # exec('a = 0', globals(), locals())와 동일
exec('print(a)') # exec('print(a)', globals(), locals())와 동일
print(locals()) # {'a': 0}
# 그러나, 여기서 print(a)는 작동하지 않을 것임
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의 다른 곳에서 논의된 상태 불일치 문제의 영향을 받았습니다.)
import sys
def f():
a = None
exec('a = 0', locals=sys._getframe().f_locals)
print(a) # 0
f()
모듈(module) 및 클래스(class) 스코프(중첩된 호출 포함)에 대한 exec()
및 eval()
의 동작은 변경되지 않습니다. 해당 스코프에서 locals()
의 동작이 변경되지 않기 때문입니다.
표준 라이브러리의 다른 코드 실행 API에 미치는 영향 (Impact on other code execution APIs in the standard library)
pdb
및 bdb
는 frame.f_locals
API를 사용하므로, 최적화된 프레임에서도 지역 변수를 안정적으로 업데이트할 수 있습니다. 이 PEP를 구현하면 디버거가 활성화된 동안 스레드, 제너레이터, 코루틴 및 기타 동시성 코드 실행 메커니즘과 관련된 이러한 모듈의 여러 오랜 버그가 해결될 것입니다.
표준 라이브러리의 다른 코드 실행 API(code
모듈 등)는 locals()
또는 frame.f_locals
에 암시적으로 접근하지 않습니다. 하지만 이 PEP의 나머지 부분에서 설명하는 바와 같이 이러한 네임스페이스를 명시적으로 전달하는 동작은 변경될 것입니다. (최적화된 스코프에서 locals()
를 전달하면 더 이상 호출 간에 코드 실행 네임스페이스가 암시적으로 공유되지 않으며, 최적화된 스코프에서 frame.f_locals
를 전달하면 지역 변수 및 비지역 셀(nonlocal cell) 참조를 안정적으로 수정할 수 있습니다.)
C API 호환성 (C API compatibility)
PyEval_GetLocals
호환성 (PyEval_GetLocals
compatibility)
PyEval_GetLocals()
는 Python 수준에서 locals()
를 에뮬레이트하는지 sys._getframe().f_locals
를 에뮬레이트하는지를 역사적으로 구분하지 않았습니다. 이는 모두 지역 변수 바인딩의 동일한 공유 캐시에 대한 참조를 반환했기 때문입니다.
이 PEP를 통해 locals()
는 최적화된 프레임에 대한 각 호출에서 독립적인 스냅샷을 반환하도록 변경됩니다. frame.f_locals
(PyFrame_GetLocals
와 함께)는 새로운 write-through proxy 인스턴스를 반환하도록 변경됩니다.
PyEval_GetLocals()
는 빌려온 참조를 반환하기 때문에, 이 두 대안 중 어느 하나에 맞춰 의미론을 업데이트하는 것이 불가능합니다. 따라서 프레임 객체에 저장된 공유 캐시 딕셔너리를 필요로 하는 유일한 남아있는 API가 됩니다.
이것이 기술적으로 함수의 의미론을 변경하지는 않지만, 다른 API 사용자가 추가 딕셔너리 항목을 볼 수 없게 합니다. 이는 해당 API가 더 이상 동일한 기본 캐시 딕셔너리에 접근하지 않기 때문입니다.
PyEval_GetLocals()
가 Python locals()
내장 함수와 동일하게 사용되는 경우, 대신 PyEval_GetFrameLocals()
를 사용해야 합니다.
이 코드는:
locals = PyEval_GetLocals();
if (locals == NULL) {
goto error_handler;
}
Py_INCREF(locals);
다음으로 대체되어야 합니다:
// Python 코드의 "locals()"와 동일
locals = PyEval_GetFrameLocals();
if (locals == NULL) {
goto error_handler;
}
PyEval_GetLocals()
가 Python에서 sys._getframe().f_locals
를 호출하는 것과 동일하게 사용되는 경우, PyEval_GetFrame()
결과에 대해 PyFrame_GetLocals()
를 호출하여 대체해야 합니다.
이러한 경우, 원본 코드는 다음으로 대체되어야 합니다.
// Python 코드의 "sys._getframe()"과 동일
frame = PyEval_GetFrame();
if (frame == NULL) {
goto error_handler;
}
// Python 코드의 "frame.f_locals"와 동일
locals = PyFrame_GetLocals(frame);
frame = NULL; // 빌려온 참조의 가시성 최소화
if (locals == NULL) {
goto error_handler;
}
PEP 709 인라인 컴프리헨션(Inlined Comprehensions)에 미치는 영향 (Impact on PEP 709 inlined comprehensions)
함수 내의 인라인 컴프리헨션의 경우, locals()
는 현재 컴프리헨션 내부 또는 외부에서 동일하게 동작하며, 이는 변경되지 않을 것입니다. 함수 내의 locals()
동작은 일반적으로 이 PEP의 나머지 부분에서 명시된 대로 변경됩니다.
모듈 또는 클래스 스코프의 인라인 컴프리헨션의 경우, 인라인 컴프리헨션 내에서 locals()
를 호출하면 각 호출마다 새로운 딕셔너리를 반환합니다. 이 PEP는 함수 내의 locals()
도 각 호출마다 항상 새로운 딕셔너리를 반환하도록 하여 일관성을 향상시킵니다. 클래스 또는 모듈 스코프의 인라인 컴프리헨션은 인라인 컴프리헨션이 여전히 별개의 함수인 것처럼 동작할 것입니다.
구현 (Implementation)
frame.f_locals
를 읽을 때마다 지역 변수(셀(cell) 및 자유(free) 변수 포함) 이름과 해당 지역 변수 값의 매핑처럼 보이는 새로운 프록시(proxy) 객체가 생성됩니다.
가능한 구현 스케치(sketch)는 다음과 같습니다. 밑줄로 시작하는 모든 속성은 보이지 않으며 직접 접근할 수 없습니다. 이는 제안된 설계를 설명하기 위한 것입니다.
C API
PyEval_GetLocals()
는 대략 다음과 같이 구현됩니다.
PyObject *PyEval_GetLocals(void) {
PyFrameObject * = ...; // 현재 프레임 가져오기.
if (frame->_locals_cache == NULL) {
frame->_locals_cache = PyEval_GetFrameLocals();
} else {
PyDict_Update(frame->_locals_cache, PyFrame_GetLocals(frame));
}
return frame->_locals_cache;
}
빌려온 참조를 반환하는 모든 함수와 마찬가지로, 참조가 객체의 수명(lifetime)을 넘어 사용되지 않도록 주의해야 합니다.
구현 노트 (Implementation Notes)
PEP 텍스트가 승인되었을 때, PyEval_GetLocals
는 새로운 write-through proxy의 캐시된 인스턴스를 반환하기 시작할 것을 제안했지만, 구현 스케치는 프레임 인스턴스에 캐시된 딕셔너리 스냅샷을 계속 반환할 것을 나타냈습니다. 이 불일치는 PEP를 구현하는 동안 확인되었으며, 스티어링 위원회(Steering Council)에서 Python 3.12의 동작을 유지하는 방향으로 해결되었습니다. (즉, 프레임 인스턴스에 캐시된 딕셔너리 스냅샷을 반환). PEP 텍스트는 이에 따라 업데이트되었습니다.
C API 명확화 논의 중에, 최적화된 스코프에서 locals()
가 독립적인 스냅샷을 반환하도록 업데이트된 이유가 명확하지 않다는 점도 드러났습니다. 이는 이 PEP에서 독립적으로 다루기보다는 원래 PEP 558 논의에서 상속되었기 때문입니다. PEP 텍스트는 이 변경 사항을 더 잘 다루도록 업데이트되었으며, Specification
및 Backwards Compatibility
섹션에 추가 업데이트가 포함되어 locals()
네임스페이스에서 코드를 실행하는 코드 실행 API에 미치는 영향을 다룹니다. 추가 동기 및 근거 세부 정보도 PEP 558에 추가되었습니다.
Python 3.13.0에서는 write-through proxy가 del
및 pop()
을 사용하여 추가 변수도 삭제하는 것을 허용하지 않았습니다. 이는 이후 호환성 회귀(compatibility regression)로 보고되었으며, 현재 ‘frame.f_locals 속성’ 섹션에서 설명하는 대로 해결되었습니다.
PEP 558과의 비교 (Comparison with 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은 또한 확장 모듈이 현재 활성 Python 스코프가 최적화되었는지 여부를 더 쉽게 판단하고, 따라서 C API의 locals()
와 동등한 것이 프레임의 지역 실행 네임스페이스에 대한 직접 참조를 반환하는지 아니면 프레임의 지역 변수 및 비지역 셀 참조의 얕은 복사본을 반환하는지 알 수 있도록 하는 몇 가지 추가 Python 스코프 인트로스펙션(introspection) 인터페이스를 C API에 도입할 것을 제안했습니다. 이러한 인트로스펙션 API를 추가할지 여부는 locals()
및 frame.f_locals
에 제안된 변경 사항과 독립적이므로, 이 PEP에는 그러한 제안이 포함되지 않았습니다.
PEP 558은 결국 이 PEP를 위해 철회되었습니다.
참조 구현 (Reference Implementation)
구현은 GitHub에서 드래프트 풀 리퀘스트(draft pull request)로 개발 중입니다.
저작권 (Copyright)
이 문서는 퍼블릭 도메인(public domain) 또는 CC0-1.0-Universal 라이선스(둘 중 더 관대한 라이선스)에 따라 제공됩니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments