Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at locals().

×

See PEP 1 for how to propose changes.

초록

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의 동작으로 인해 이상한 bugs가 발생할 수 있습니다.

이 PEP를 적용하면 두 예제 모두 2를 출력합니다. 함수 수준의 변경이 캐시된 딕셔너리 스냅샷이 아니라 프레임의 최적화된 지역 변수에 직접 기록되기 때문입니다.

Python 3.12의 동작에는 이를 상쇄할 만한 이점이 없습니다. 신뢰할 수 없고 느립니다.

locals() 내장 함수에는 자체적으로 바람직하지 않은 동작이 있습니다. 이러한 문제에 대한 자세한 내용은 PEP 558을 참조하십시오.

근거

frame.f_locals 속성을 쓰기 전달 프록시로 만들기

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에서는 optimized scopes:에서 locals() 내장 함수의 동작을 표준화하기 위한 세 가지 잠재적 방안을 검토했습니다.

  • 주어진 프레임에서 locals()를 호출할 때마다 지역 변수의 단일 공유 스냅샷을 갱신하는 기존 동작을 유지합니다.
  • locals()frame.f_locals와 유사한 쓰기 관통 프록시 인스턴스를 반환하도록 합니다.
  • locals()가 진정으로 독립적인 스냅샷을 반환하도록 하여, exec()를 통해 지역 변수의 값을 변경하려는 시도가 일부 상황에서 허용되는 대신 일관되게 무시되도록 하십시오.

언어 레퍼런스에서 가장 쉽게 설명하고 사용자가 기억할 수 있는 선택지로 마지막 옵션이 선택되었습니다:

  • locals()내장 함수는 최적화된 스코프에서는 지역 변수의 즉각적인 스냅샷을 제공하고, 다른 스코프에서는 읽기/쓰기 접근을 제공합니다. 그리고
  • frame.f_locals는 최적화된 스코프를 포함한 모든 스코프에서 지역 변수에 대한 읽기/쓰기 접근을 제공합니다.

이 접근 방식을 사용하면 두 API가 최적화된 스코프에서 완전한 읽기/쓰기 접근을 제공하는 경우보다, 쓰기 접근이 필요하지 않거나 의도되지 않은 경우에도 코드의 의도를 더 명확하게 표현할 수 있습니다. 이 설계 결정에 대한 자세한 내용은 PEP 558, 특히 동기 섹션과 최적화된 스코프에서 eval()과 exec()에 대한 추가 고려 사항를 참조하십시오.

이 접근 방식에 단점이 없는 것은 아니며, 이러한 단점은 아래의 하위 호환성 섹션에서 다룹니다.

명세

Python API

frame.f_locals속성

모듈 및 클래스 스코프(exec()eval()호출 포함)에서 frame.f_locals는 코드 실행에 사용되는 지역 변수 네임스페이스에 대한 직접 참조입니다.

함수 스코프(및 기타 optimized scopes)에서는 기본 프레임의 최적화된 지역 변수 저장 배열과 비지역 변수에 대한 모든 셀 참조의 내용을 직접 수정할 수 있는 새로운 쓰기 관통 프록시 타입의 인스턴스가 됩니다.

뷰 객체는 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 및 이전 버전에서는 eval()또는 exec()locals를 제공하면서 globals도 함께 제공하지 않는 것은 불가능했습니다. 이전에는 이 인자들이 위치 전용 인자였기 때문입니다. 이 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에 액세스할 때 위에서 설명한 것과 같습니다.

이 함수는 새로운 참조를 반환하므로 최적화된 스코프에서 호출할 때마다 새로운 쓰기 통과 프록시 인스턴스를 생성할 수 있습니다.

더 이상 사용되지 않는 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()뿐입니다. 따라서 PyEval_GetLocals()호출은 Python 3.14에서 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의 변경 사항에 따르면, 명시적으로 호출한 locals()exec() 호출에서 암시적으로 사용된 스냅샷과 별개의 스냅샷을 생성하므로, 함수에서 x가 정의된 로컬 변수인지 여부와 관계없이 이는 항상 None을 출력합니다.

Python 3.12에서는 표시된 정확한 예제가 1을 출력하지만, 관련 없어 보이는 함수 정의의 변경으로 인해 대신 None을 출력할 수 있습니다(PEP 558의 (최적화된 스코프에서 eval()과 exec()에 대한 추가 고려 사항에서는 이 주제를 더 자세히 다룹니다).

eval()exec() 변경 사항

eval()exec()에 영향을 미치는 주요 변경 사항은 “locals() 변경 사항” 예제에 나와 있습니다. 최적화된 스코프에서 locals()에 반복적으로 액세스해도 더 이상 공통 기저 네임스페이스를 암시적으로 공유하지 않습니다.

C API 변경 사항

PyFrame_GetLocals 변경 사항

exec()eval()은 임의의 매핑을 locals 인자로 허용하고, 메타클래스는 __prepare__ 메서드에서 임의의 매핑을 반환할 수 있으므로, PyFrame_GetLocals는 Python 3.12에서도 이미 임의의 매핑을 반환할 수 있습니다.

최적화된 스코프에서 프레임 로컬 프록시를 반환하는 것은 내장 딕셔너리가 아닌 다른 것이 반환되는 또 하나의 경우를 추가할 뿐입니다.

PyEval_GetLocals 변경 사항

PyEval_GetLocals()의 의미는 기술적으로 변경되지 않았지만, 최적화된 프레임에 캐시된 딕셔너리가 더 이상 프레임 로컬에 액세스하는 다른 메커니즘(locals() 내장 함수, PyFrame_GetLocals 함수, 프레임 f_locals 특성)과 공유되지 않으므로 실제 동작은 변경됩니다.

하위 호환성

Python API 호환성

Python 3.12를 포함하여 그 이전 버전에서 사용된 구현에는 많은 예외적인 경우와 특이점이 있습니다. 이러한 문제를 우회하는 코드는 변경해야 할 수 있습니다. 간단한 템플릿 작성이나 출력 디버깅에 locals()를 사용하는 코드는 계속 올바르게 작동합니다. f_locals를 사용하여 로컬 변수를 수정하는 디버거와 기타 도구는 스레드 코드, 코루틴 및 제너레이터가 존재하는 경우에도 이제 올바르게 작동합니다.

frame.f_locals 호환성

f.f_locals는 함수의 네임스페이스인 것처럼 동작하지만, 관찰 가능한 차이도 일부 존재합니다. 예를 들어 최적화된 프레임에서는 f.f_locals is f.f_localsFalse입니다. 특성에 액세스할 때마다 새로운 쓰기 반영 프록시 인스턴스가 생성되기 때문입니다.

그러나 f.f_locals == f.f_localsTrue이며, 매핑 키로 새 변수 이름을 추가하는 것을 포함하여 어떤 방식으로든 기저 변수에 가해진 모든 변경 사항이 항상 표시됩니다.

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 이전 버전에서만 명시적으로 복사하는 버전 종속 헬퍼 함수를 정의해야 합니다.:

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에 미치는 영향

pdbbdbframe.f_localsAPI를 사용하므로 최적화된 프레임에서도 지역 변수를 안정적으로 업데이트할 수 있습니다. 이 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_localsPyFrame_GetLocals와 함께 새로운 쓰기 전달 프록시 인스턴스를 반환하도록 변경됩니다.

PyEval_GetLocals()는 빌린 참조를 반환하므로 그 의미를 이러한 대안 중 하나에 맞게 변경할 수 없습니다. 따라서 프레임 객체에 저장된 공유 캐시 딕셔너리를 요구하는 유일한 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;
}

차용된 참조를 반환하는 모든 함수와 마찬가지로, 객체의 수명이 지난 후에도 해당 참조가 사용되지 않도록 주의를 기울여야 합니다.

구현 참고 사항

PEP가 승인되었을 때 PEP 본문에서는 PyEval_GetLocals가 새로운 쓰기 전달 프록시의 캐시된 인스턴스를 반환하기 시작할 것이라고 제안했지만, 구현 개요에서는 프레임 인스턴스에 캐시된 딕셔너리 스냅샷을 계속 반환할 것이라고 설명했습니다. 이 불일치는 PEP를 구현하는 과정에서 확인되었으며, 프레임 인스턴스에 캐시된 딕셔너리 스냅샷을 반환하는 Python 3.12의 동작을 유지하는 방향으로 Steering Council에서 해결되었습니다. 이에 따라 PEP 본문이 업데이트되었습니다.

C API 명확화에 관한 논의 중에, optimized scopes에서 locals()가 독립적인 스냅샷을 반환하도록 변경된 근거가 명확하지 않다는 점도 드러났습니다. 이는 이 PEP에서 독립적으로 다루어진 것이 아니라 원래 PEP 558 논의에서 이어져 왔기 때문입니다. 이러한 변경을 더 잘 다루도록 PEP 본문이 업데이트되었으며, 기본적으로 locals() 네임스페이스에서 코드를 실행하는 코드 실행 API에 미치는 영향을 다루기 위해 명세 및 하위 호환성 섹션도 추가로 업데이트되었습니다. 추가적인 동기와 근거에 관한 세부 사항도 PEP 558에 추가되었습니다.

3.13.0에서는 쓰기 전달 프록시를 사용하더라도 delpop()으로 추가 변수조차 삭제할 수 없었습니다. 이후 이는 호환성 회귀로 보고되었으며, 현재 frame.f_locals속성에 설명된 방식으로 해결되었습니다.

PEP 558과의 비교

이 PEP와 PEP 558locals()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()에 해당하는 기능이 프레임의 로컬 실행 네임스페이스에 대한 직접 참조를 반환하는지, 아니면 프레임의 로컬 변수와 비로컬 셀 참조의 얕은 복사본을 반환하는지를 판단할 수 있습니다. 이러한 검사 API를 추가할지 여부는 locals()frame.f_locals에 제안된 변경 사항과는 독립적이므로, 이 PEP에는 그러한 제안이 포함되지 않았습니다.

PEP 558은 이 PEP를 채택하는 방향으로 ultimately withdrawn되었습니다.

참조 구현

구현은 GitHub의 초안 풀 리퀘스트로 개발 중입니다.