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
번역·라이선스 안내
이 비공식 한국어 번역은 원문 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은 이미 이러한 가능성을 보여 주고 있습니다. 핍홀 최적화기는 상수 집합 디스플레이를 frozenset으로 재작성하지만, in의 오른쪽 피연산자일 때만 그렇게 합니다. 같은 디스플레이를 변수에 대입하면 이 최적화는 사라집니다. 근본 원인은 컴파일러가 set 또는 dict 디스플레이의 불변성을 결코 증명할 수 없으므로, 실행될 때마다 이를 다시 만들어야 한다는 데 있습니다. 의미가 불변성을 보장하는 디스플레이는 그 장벽을 완전히 없애 줍니다.
이 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 접두사는 frozen을 뜻하며, 익숙한 f-문자열 접두사 관례를 따릅니다. f-문자열과 같은 글자를 공유하는 것은 문제가 되지 않습니다. 문자열도 불변이므로, 어느 경우든 f 접두사가 붙은 표현식은 불변 값으로 평가됩니다. f{는 현재의 모든 Python 버전에서 문법 오류이므로, 이 문법은 완전한 하위 호환성을 가집니다. 토크나이저는 f{에 대해 단일 FLBRACE 토큰을 내보내므로, (공백이 있는) f {1}은 여전히 오류이며 이름 f나 f-문자열과 혼동될 여지가 없습니다.
명세
문법
우리의 목표는 새 구문과 문법을 기존 set 및 dict 구문, 문법 규칙과 동일하게 만드는 것입니다.
set 및 dict 디스플레이와 컴프리헨션을 본떠 atom에 새로운 대안이 추가됩니다:
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}는 ({*xs}처럼) frozenset 디스플레이이고f{**d}는 ({**d}처럼) frozendict 디스플레이입니다. - 컴프리헨션이 지원됩니다.
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입니다.
별표 언패킹을 사용하거나 스택 사용 지침을 초과하는 디스플레이는 가변 컨테이너를 만든 뒤 제자리에서 동결하는 방식으로 대체됩니다. 결과는 구별할 수 없습니다. 컴프리헨션도 같은 경로를 따르므로 새로운 옵코드가 필요하지 않습니다.
최적화 파이프라인
이 PEP의 핵심 주장은 frozen 디스플레이가 단지 편리한 문법에 그치지 않는다는 것입니다. frozen 디스플레이는 컴파일러의 모든 단계에 그 단계가 활용할 수 있는 보장을 제공합니다. 참조 구현은 이미 전체 파이프라인을 거치고 있습니다:
- 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 프로젝트는 현재 보류 중입니다.
이 파이프라인은 가변 디스플레이로는 결코 지원할 수 없는 향후 작업의 길도 엽니다. 폴딩된 동결 상수를 여러 코드 객체가 공유하는 것과, 자유 스레딩 환경에서 이 상수를 불멸 객체로 만드는 것입니다.
하위 호환성
f{는 현재 문법 오류이므로, 기존 코드의 의미가 바뀌는 경우는 없습니다. 도구에서 볼 수 있는 변경 사항은 새로운 FLBRACE 토큰, 새로운 AST 노드 타입 네 가지, 새로운 옵코드 두 개, 그리고 바이트코드 매직 넘버 증가입니다.
교육 방법
“집합 또는 딕셔너리 디스플레이 앞에 f를 붙이면 동결됩니다”. 스타일 지침: Python 3.16 이상에서 리터럴 값에는 frozenset({...})보다 f{...}를 사용하십시오. 상수 동결 디스플레이는 첫 실행 이후에는 비용이 들지 않습니다.
표준 라이브러리에 미치는 영향
표준 라이브러리를 (테스트를 제외하고) 간단히 조사해 보면 frozenset(...) 호출 지점이 약 105개, frozendict(...) 호출 지점이 약 65개 있으며, 이 중 각각 약 46개와 22개가 리터럴 디스플레이를 전달하므로 f{...}로 작성할 수 있습니다. 이들은 typing, dataclasses, functools, copy, traceback 같은 널리 쓰이는 모듈 전반에 퍼져 있습니다.
이 수치는 잠재적 효과를 추정한 것일 뿐입니다. 이 PEP는 표준 라이브러리를 기계적으로 재작성할 것을 제안하지 않습니다.
참조 구현
파서, AST, 코드 제너레이터, CFG 상수 폴딩을 포함한 전체 구현은 저자의 CPython 포크에 있는 fset_fdict 브랜치에서 확인할 수 있습니다.
기각된 아이디어
대안 표기
많은 표기가 검토되었습니다. f가 선택된 이유는 단순히 frozen을 가장 잘 연상시키는 접두사이기 때문입니다.
- 한 글자 접두사:
i{'key': 1}(immutable, 불변),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-문자열에서처럼 F{를 사용할 수 있게 하는 방안도 기각했습니다. 이는 문법을 최대한 간결하게 유지하고 별도의 F{ 토큰을 만들지 않기 위해 거부되었습니다.
동결 메서드
{'key': 1}.freeze()나 {'key': 1}.take_frozendict() 같은 메서드는 진정한 대안이 아닙니다. 이러한 메서드는 이 PEP와 별개로 추가될 수 있습니다.
이러한 메서드는 그 자체로 훌륭한 기능이 될 수 있지만, 이를 불변 컨테이너를 만드는 유일한 방법으로 삼는 것은 선택지가 될 수 없습니다:
- 이러한 메서드를 사용한 여러 줄 표현식은 객체의 타입을 알기 위해 맨 마지막 줄까지 읽어야 하므로 읽기가 매우 어렵습니다.
- 일반적인 경우에 작성하기에는 상당히 장황합니다.
- 정적 분석 도구에 문법적 보장을 제공하지 않습니다.
a = {1: 2}; b(a.take_frozenset())처럼 사용할 때 다른 의미를 가질 수 있습니다. 내부 구현이 어떤 모습이 될지에 따라a가 비워진다는 의미가 될 수도 있습니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.