PEP 841 – 불변 타입 최적화를 위한 동결 구문 추가
- Author:
- Donghee Na <donghee.na at python.org>, Nikita Sobolev <mail at sobolevn.me>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Created:
- 20-Jul-2026
- Python-Version:
- 3.16
- Post-History:
- 20-Jul-2026
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 동결 디스플레이 구문을 제안합니다. f{1, 2, 3}은 frozenset으로 평가되고, f{'a': 1}은 frozendict로 평가됩니다. 불변성이 사용 방식에서 추론되는 것이 아니라 구문 자체로 보장되므로, 컴파일러는 동결 디스플레이를 최적화 파이프라인의 일급 구성 요소로 취급할 수 있습니다. 즉, 상수 디스플레이는 컴파일 시 정확한 결과 타입을 갖는 단일 LOAD_CONST로 접혀 .pyc 파일에 캐시됩니다.
동기
Python에는 가변 컨테이너를 위한 디스플레이 구문은 있지만 불변 컨테이너를 위한 구문은 없습니다. 현재 불변 집합은 frozenset({1, 2, 3})으로, 불변 매핑은 frozendict({'a': 1})으로 작성해야 합니다. 각각 다음과 같은 문제점이 있습니다.
- 가변 집합이나 딕셔너리를 만든 다음 불변 타입으로 복사합니다.
- 실행할 때마다 런타임에
frozenset이나frozendict라는 이름을 조회합니다. - 두 이름 중 어느 것이든 재바인딩될 수 있고 호출에 임의의 부작용이 있을 수 있으므로, 또는 값이 최적화에 예상하지 못한 타입일 수 있고 고유하게 참조되지 않을 수 있으므로 컴파일러가 쉽게 최적화할 수 없습니다.
CPython은 이미 이러한 가능성을 보여 줍니다. peephole 최적화 프로그램은 상수 집합 디스플레이를 frozenset으로 다시 작성하지만, in의 오른쪽 피연산자인 경우에만 그렇게 합니다. 같은 디스플레이를 변수에 할당하면 최적화가 사라집니다. 근본 원인은 컴파일러가 set이나 dict디스플레이의 불변성을 결코 증명할 수 없으므로 실행할 때마다 이를 다시 만들어야 한다는 것입니다. semantics가 불변성을 보장하는 디스플레이는 이러한 장벽을 완전히 제거합니다.
이 PEP의 목표 중 하나는 CPython에서 불변 컨테이너의 사용을 늘려, 프리 스레딩과 서브인터프리터가 훨씬 더 일반화되는 동시성 중심의 미래를 준비하는 것입니다. 불변 데이터 구조를 효율적으로 생성하고 편리한 구문을 제공하면 frozendict와 frozenset의 사용이 촉진될 것입니다. 이를 통해 공유 가변 컨테이너의 우발적인 변경으로 발생하는 버그를 줄이는 동시에 성능도 향상할 수 있습니다. 예를 들어 서브인터프리터는 이미 frozenset 객체를 공유하고 있으며, frozendict 객체도 공유할 계획이 있습니다.
불변 컨테이너 디스플레이는 커뮤니티에서 여러 차례 요청되고 논의되었으며, 최근에는 Discourse의 frozenset and frozendict comprehensions 스레드에서 논의되었습니다.
근거
내장 함수 호출이 아닌 구문
구문만이 컴파일러에 의미론적 보장을 제공합니다. frozenset(...)호출은 가려질 수 있지만 동결 디스플레이는 가려질 수 없습니다. 아래에서 설명하는 모든 최적화는 이 단일 속성에서 비롯됩니다.
정적 분석도 같은 방식으로 이점을 얻습니다. 의미론적 분석을 수행하지 않는 도구는 현재 frozenset(...)가 내장 함수를 가리킨다고 가정해야 합니다. 동결 디스플레이는 이러한 가정을 구문적 보장으로 바꾸므로, 분석기는 완전한 확신을 가지고 결과를 불변으로 취급할 수 있습니다. 이는 이름 확인을 전혀 수행하지 않는 순수 구문 도구에도 적용됩니다.
린터와 포매터는 최신 Python 버전에서 frozenset({1, 2, 3})와 frozendict({1: 2})를 각각 f{1, 2, 3}와 f{1: 2}로 자동으로 다시 작성할 수 있습니다.
왜 f{...}인가
f 접두사는 익숙한 f-문자열 접두사 규칙을 반영하여 frozen을 의미합니다. f-문자열과 문자를 공유하는 것은 문제가 되지 않습니다. 문자열도 불변이므로 어느 경우든 f 접두사가 붙은 표현식은 불변 값으로 평가됩니다. f{는 현재 모든 Python 버전에서 구문 오류이므로 이 구문은 완전한 하위 호환성을 갖습니다. 토크나이저는 f{에 대해 단일 FLBRACE 토큰을 방출하므로, f {1} (공백 포함)는 여전히 오류이며 이름 f 또는 f-문자열과 모호하지 않습니다.
사양
문법
목표는 새로운 구문과 문법을 기존 set 및 dict 구문과 문법 규칙과 동일하게 만드는 것입니다.
atom에 새로운 대안이 추가되며, set 및 dict 디스플레이와 컴프리헨션을 반영합니다.
fset: FLBRACE star_named_expressions '}'
fsetcomp: FLBRACE star_named_expression for_if_clauses '}'
fdict: FLBRACE [double_starred_kvpairs] '}'
fdictcomp: FLBRACE kvpair for_if_clauses '}'
f{1, 2, 3}은 frozenset 디스플레이입니다.f{'a': 1, 'b': 2}은 frozendict 디스플레이입니다.f{}는{}를 반영하는 빈 frozendict입니다.- 별표 언패킹은 기존 디스플레이를 따릅니다.
f{*xs}는 frozenset 디스플레이({*xs}와 같음)이고,f{**d}는 frozendict 디스플레이({**d}와 같음)입니다. - 컴프리헨션이 지원됩니다.
f{x for x in xs}는 frozenset 컴프리헨션이고,f{k: v for k, v in items}는 frozendict 컴프리헨션입니다. - 비동기 컨텍스트에서는 비동기 컴프리헨션도 지원됩니다.
f{x async for arange(5)}는 frozenset 비동기 컴프리헨션이고,f{k: v async for k, v in items}는 frozendict 비동기 컴프리헨션입니다. - PEP 798 및 컴프리헨션에서의 언패킹도 지원됩니다.
f{*nums for nums in list_of_nums}는 언패킹을 포함한 frozenset 컴프리헨션이고,f{**items for nums in list_of_items}는 언패킹을 포함한 frozendict 컴프리헨션입니다.
AST
네 개의 새로운 표현식 노드가 추가됩니다: FrozenSet(elts), FrozenDict(keys, values), FrozenSetComp(elt, generators), FrozenDictComp(key, value, generators)이며, 구조적으로 해당 가변 대응 노드와 동일합니다. 서로 구별되는 노드는 플래그를 사용하는 대신 모든 후속 소비자(예: 심볼 테이블, AST 최적화기, 코드 제너레이터 및 서드파티 도구)가 불변성에 따라 직접 디스패치하도록 합니다.
의미론
frozenset 디스플레이는 frozenset({...})이 반환하는 것과 정확히 동일하게 평가됩니다. frozendict 디스플레이는 frozendict({...})이 반환하는 것과 정확히 동일하게 평가됩니다. 두 결과 유형 모두 불변이고 해시 가능하므로, 아래의 컴파일 시점 처리가 타당해집니다.
바이트코드
두 개의 새로운 명령어가 추가됩니다.
BUILD_FROZENSET (count)는BUILD_SET처럼 작동하지만, 새로 생성되어 유일하게 참조되는 집합을 복사 없이 제자리에서 동결합니다.BUILD_FROZENMAP (count)은BUILD_MAP처럼 작동하지만frozendict를 생성합니다.
count는 기존 BUILD_SET 및 BUILD_MAP 명령어와 동일한 형식과 의미를 갖는 일반적인 oparg입니다.
별표 언패킹을 사용하거나 스택 사용 지침을 초과하는 디스플레이는 가변 컨테이너를 생성한 다음 제자리에서 동결하는 방식으로 대체됩니다. 결과는 구별할 수 없습니다. 컴프리헨션도 동일한 경로를 따르므로 새로운 opcode가 필요하지 않습니다.
최적화 파이프라인
이 PEP의 핵심 주장은 동결된 디스플레이가 단순히 편리한 구문에 그치지 않는다는 것입니다. 동결된 디스플레이는 컴파일러의 모든 단계가 활용할 수 있는 보장을 제공합니다. 참조 구현은 이미 전체 파이프라인을 실행합니다.
- AST 전처리.
FrozenSet와FrozenDict는 요소의 AST 수준 상수 폴딩에 참여합니다. - 코드 생성. 일반적인 경우에는 이름 조회, 임시 복사 없이 단일
BUILD_FROZENSET/BUILD_FROZENDICT명령어로 컴파일되며, 정적으로 정확히 알려진 결과 타입을 사용합니다(예를 들어 컴파일러의 타입 추론에서 컴파일 시점에f{1, 2}[0]를 거부하는 데 사용합니다). - 제어 흐름 그래프(CFG) 상수 폴딩. 키와 값이 모두 상수인 디스플레이는 하나의
LOAD_CONST로 폴딩되고, marshal에 의해.pyc에 직렬화되며, 모든 실행에서 공유됩니다. 따라서 실행마다 구성하는 비용이 전혀 없습니다. 기존 리스트/세트 폴딩과 달리 이는 무조건적으로 유효합니다. 불변성은 값이 사용되는 방식이 아니라 언어 의미론에서 비롯되기 때문입니다. 예를 들어f{'key': ['list']}와 같이 상수가 아닌 요소가 있는 디스플레이는 런타임에 계속 구성됩니다. - 상수 중복 제거. 동결된 상수는
co_consts중복 제거에 참여합니다. frozendict의 경우 중복 제거 키가 삽입 순서를 유지하므로, 디스플레이가 키 순서가 다른 동일한 디스플레이 때문에 자신의 순회 순서를 잃는 일은 없습니다.
Note
이 PEP는 JIT 수준의 최적화에 대해 의도적으로 어떠한 주장도 하지 않습니다. JIT 프로젝트는 Steering Council’s announcement에 따라 현재 중단된 상태입니다.
또한 이 파이프라인은 변경 가능한 디스플레이가 지원할 수 없는 향후 작업을 가능하게 합니다. 코드 객체 간에 폴딩된 동결 상수를 공유하고 자유 스레딩 환경에서 이를 불멸화하는 작업입니다.
하위 호환성
f{는 현재 구문 오류이므로 기존 코드의 의미는 변경되지 않습니다. 도구에서 확인할 수 있는 변경 사항은 새로운 FLBRACE 토큰, 네 가지 새로운 AST 노드 유형, 두 가지 새로운 옵코드 및 바이트코드 매직 넘버 증가입니다.
이 내용을 가르치는 방법
“세트 또는 딕셔너리 디스플레이 앞에 f를 접두사로 붙여 동결된 것으로 만듭니다”. 스타일 지침: Python 3.16 이상에서 리터럴 값에는 frozenset({...})보다 f{...}를 선호하십시오. 상수 동결 디스플레이는 첫 번째 실행 이후 비용이 들지 않습니다.
표준 라이브러리에 미치는 영향
표준 라이브러리를 간단히 조사한 결과(테스트 제외), 약 105개의 frozenset(...) 및 65개의 frozendict(...) 호출 지점이 발견되었으며, 이 중 각각 약 46개와 22개는 리터럴 디스플레이를 전달하므로 f{...}로 작성할 수 있습니다. 이러한 호출 지점은 typing, dataclasses, functools, copy 및 traceback과 같이 널리 사용되는 모듈 전반에 분포합니다.
이 수치는 잠재적 영향에 대한 추정치일 뿐입니다. 이 PEP는 표준 라이브러리의 기계적 재작성을 제안하지 않습니다.
참조 구현
파서, AST, 코드 제너레이터 및 CFG 상수 폴딩을 포함한 완전한 구현은 저자의 CPython 포크에 있는 fset_fdict branch에서 사용할 수 있습니다.
거부된 아이디어
대체 표기
많은 표기가 고려되었습니다. f는 frozen을 가장 잘 연상시키는 접두사이기 때문에 단순히 선택되었습니다.
- 한 글자 접두사:
i{'key': 1}(불변),z{'key': 1}. 형식이frozen이라고 불리기 때문에 이는 거부되었으며, 접두사로는f가 가장 잘 읽힙니다. - 여러 글자 접두사:
fr{'key': 1},fz{'key': 1},frz{'key': 1}. 이는 실제로 추가되는 가치 없이 작성하고 읽을 글자만 하나 더 늘리기 때문에 거부했습니다. - 기호 접두사:
${'key': 1},+{'key': 1}. 대부분의 기호가 이미 연산자로서 의미를 가지기 때문에 이는 거부되었습니다. 이러한 기호를 이 새로운 용도로 재사용할 수는 없습니다. 향후 다른 용도로 사용하기 위해$같은 새 기호를 추가하고 싶지도 않습니다. - 괄호 변형:
{{'key': 1}},|{'key': 1}|,{|'key': 1|}. 단일 토큰f{를 추가하는 것이 토큰 두 개를 추가하는 것보다 쉽기 때문에 이는 거부되었습니다.f{를 작성하는 것도 서로 다른 두 괄호를 작성하는 것보다 쉽습니다.{{}}는 현재 유효한 구문이므로 거부되었습니다. - 단어 접두사:
frozen{'key': 1},fdict{'key': 1},frozendict {'key': 1}. 상당히 장황하기 때문에 이는 거부되었습니다.
f-string에서는 가능하므로 F{를 사용하는 것도 거부했습니다. 구문을 가능한 한 간결하게 유지하고 추가적인 F{ 토큰을 만들지 않기 위해 이는 거부되었습니다.
동결 메서드
{'key': 1}.freeze() 또는 {'key': 1}.take_frozendict()와 같은 메서드는 진정한 대안이 아닙니다. 이러한 메서드는 이 PEP와 독립적으로 추가할 수 있습니다.
이러한 메서드는 그 자체로 훌륭한 기능이 될 수 있지만, 이것을 불변 컨테이너를 만드는 유일한 방법으로 제한하는 것은 선택지가 아닙니다.
- 이러한 메서드를 사용하는 여러 줄 표현식은 객체의 타입을 알기 위해 맨 마지막 줄까지 읽어야 하므로 읽기가 매우 어렵습니다.
- 일반적인 경우에 작성하기에는 상당히 장황합니다.
- 정적 분석 도구에 대한 구문 보장을 제공하지 않습니다.
a = {1: 2}; b(a.take_frozenset())로 사용할 때 how the internals would look like 에 따라 의미가 달라질 수 있으며,a가 비워진다는 의미일 수도 있습니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.