[Accepted] PEP 701 - Syntactic formalization of f-strings
원문 링크: PEP 701 - Syntactic formalization of f-strings
상태: Accepted 유형: Standards Track 작성일: 15-Nov-2022
PEP 701 – f-string의 문법적 형식화 (Syntactic formalization of f-strings)
- 저자: Pablo Galindo, Batuhan Taskaya, Lysandros Nikolaou, Marta Gómez Macías
- 상태: Accepted (수락됨)
- 유형: Standards Track
- 생성일: 2022년 11월 15일
- Python 버전: 3.12
- 최종 해결일: 2023년 3월 14일
요약 (Abstract)
이 문서는 PEP 498에서 원래 제시되었던 일부 제한 사항을 해제하고, 파서(parser)에 직접 통합될 수 있는 f-string을 위한 공식화된 문법(grammar)을 제공할 것을 제안합니다. f-string의 제안된 문법 형식화는 f-string이 파싱(parsing)되고 해석되는 방식에 몇 가지 작은 부작용을 미치겠지만, 최종 사용자 및 라이브러리 개발자에게 상당한 이점을 제공하며, f-string 파싱 전용 코드의 유지보수 비용을 획기적으로 줄여줄 것입니다.
동기 (Motivation)
f-string이 PEP 498에서 처음 도입될 때, f-string에 대한 공식적인 문법은 제공되지 않았습니다. 또한, 기존 렉서(lexer)를 수정하지 않고 CPython에 f-string 파싱을 구현하기 위해 여러 제한 사항이 부과되었습니다. 이러한 제한 사항들은 이전에도 인지되었고, PEP 536에서 이를 해제하려는 시도가 있었으나 구현되지 않았습니다. (PEP 536에서 원래 수집된) 이러한 제한 사항 중 일부는 다음과 같습니다.
- f-string을 구분하는 인용 부호(quote character)를 표현식 부분 내에서 사용하는 것이 불가능합니다.
>>> f'Magic wand: { bag['wand'] }' ^ SyntaxError: invalid syntax
- 이전에 고려되었던 해결책은 실행되는 코드에서 이스케이프 시퀀스(escape sequences)를 유발하므로 f-string에서는 금지됩니다.
>>> f'Magic wand { bag[\'wand\'] } string' SyntaxError: f-string expression portion cannot include a backslash
- 여러 줄 f-string에서도 주석(comments)이 금지되었습니다.
>>> f'''A complex trick: { ... bag['bag'] # recursive bags! ... }''' SyntaxError: f-string expression part cannot include '#'
단순히 변수 이름 대신 표현식을 사용하는 문자열 삽입(string interpolation) 방식을 채택하는 많은 다른 언어에서는 이스케이프 시퀀스 확장을 제외한 임의의 표현식 중첩(nesting)이 가능합니다. 예시는 다음과 같습니다.
- Ruby:
"#{"#{1+2}"}"
- JavaScript:
`${`${1+2}`}`
- Swift:
"\("\(1+2)")"
- C#:
$"{$"{1+2}"}"
이러한 제한 사항들은 언어 사용자 관점에서 아무런 목적이 없으며, 예외 없이 f-string 리터럴에 정규 문법(regular grammar)을 부여하고 전용 파스 코드(parse code)를 사용하여 구현함으로써 해제될 수 있습니다.
f-string의 또 다른 문제는 CPython의 현재 구현이 f-string을 STRING
토큰으로 토큰화(tokenizing)하고 이러한 토큰들을 후처리(post processing)하는 방식에 의존한다는 점입니다. 이는 다음과 같은 문제를 야기합니다.
- CPython 파서에 상당한 유지보수 비용을 추가합니다. 파싱 코드를 수동으로 작성해야 하는데, 이는 역사적으로 상당한 불일치와 버그로 이어졌습니다. C 언어로 파싱 코드를 수동으로 작성하고 유지하는 것은 기존 렉서 버퍼에 대한 많은 수동 메모리 관리가 필요하므로 항상 오류 발생 가능성이 높고 위험하다고 여겨져 왔습니다.
- f-string 파싱 코드는 PEP 617에서 처음 도입된 새로운 PEG 파서(parser)가 제공하는 개선된 오류 메시지 메커니즘을 사용할 수 없습니다. 이러한 오류 메시지의 개선은 크게 환영받았지만, f-string은 파싱 메커니즘의 별도 부분에서 파싱되기 때문에 이러한 혜택을 누릴 수 없습니다. 특히, 표현식 부분 내에서 발생하는 다른 암시적 토큰화(implicit tokenization)로 인해 혼동될 수 있는 f-string의 여러 구문 기능이 있기 때문에 이는 유감스러운 일입니다(예:
f"{y:=3}"
은 할당 표현식이 아닙니다). - 다른 Python 구현체들은 f-string이 공식 Python 문법의 일부가 아니기 때문에, f-string을 올바르게 구현했는지 알 방법이 없습니다. PyPy와 같은 여러 주요 대체 구현체들은 CPython의 PEG 파서를 사용하거나 공식 PEG 문법에 기반을 두고 있기 때문에 이는 중요합니다. f-string이 별도의 파서를 사용한다는 사실은 이러한 대체 구현체들이 공식 문법을 활용하고 문법에서 파생된 오류 메시지 개선의 이점을 누리는 것을 방해합니다.
이 제안의 한 버전은 Python-Dev에서 논의되었고 Python Language Summit 2022에서 발표되어 열렬한 반응을 얻었습니다.
배경 (Rationale)
이 PEP는 새로운 Python PEG 파서(PEP 617)를 기반으로 “f-string”을 재정의하며, 특히 문자열 구성 요소와 표현식(또는 대체, {...}
) 구성 요소의 명확한 분리를 강조합니다. PEP 498은 “f-string”의 구문 부분을 다음과 같이 요약합니다.
Python 소스 코드에서 f-string은 ‘f’가 접두사로 붙고 중괄호 안에 표현식을 포함하는 리터럴 문자열입니다. 표현식은 해당 값으로 대체됩니다.
그러나 PEP 498은 표현식 구성 요소 내부에 포함될 수 없는 것에 대한 공식적인 제외 목록도 포함하고 있었습니다(주로 기존 파서의 한계 때문입니다). 공식 문법을 명확히 확립함으로써, 우리는 이제 구현 세부 사항으로 인한 제약에 얽매이지 않고 f-string의 표현식 구성 요소를 진정으로 “모든 적용 가능한 Python 표현식”(해당 특정 컨텍스트에서)으로 정의할 수 있게 되었습니다.
형식화 노력과 위 전제는 모호한 제한 사항을 단순화하고 제거할 수 있는 능력 덕분에 Python 프로그래머에게도 상당한 이점을 제공합니다. 이는 f-string 리터럴(및 일반적인 Python 언어)의 정신적 부담과 인지 복잡성을 줄여줍니다.
표현식 구성 요소는 일반적인 Python 표현식이 포함할 수 있는 모든 문자열 리터럴을 포함할 수 있습니다. 이는 f-string의 표현식 구성 요소 내부에 동일한 인용 부호 유형(및 길이)으로 문자열 리터럴(형식화되었거나 그렇지 않은)을 중첩할 가능성을 열어줍니다.
>>> f"These are the things: {", ".join(things)}"
>>> f"{source.removesuffix(".py")}.c: $(srcdir)/{source}"
>>> f"{f"{f"infinite"}"}" + " " + f"{f"nesting!!!"}"
이 “기능”은 보편적으로 바람직하다고 동의되지 않으며, 일부 사용자들은 이를 읽기 어렵다고 생각합니다. 이에 대한 다양한 관점은 “인용 부호 재사용 관련 고려 사항” 섹션을 참조하십시오.
대부분의 사람들에게 직관적이지 않게 느껴졌던 또 다른 문제는 f-string의 표현식 구성 요소 내에서 백슬래시(backslashes)를 지원하지 않는다는 점입니다. 끊임없이 제기되는 한 가지 예시는 컨테이너(container)를 결합하기 위해 표현식 부분에 개행 문자(newline character)를 포함하는 것입니다.
>>> a = ["hello", "world"]
>>> f"{'\n'.join(a)}"
File "<stdin>", line 1
f"{'\n'.join(a)}"
^
SyntaxError: f-string expression part cannot include a backslash
이에 대한 일반적인 해결 방법은 개행 문자를 중간 변수에 할당하거나, f-string을 생성하기 전에 전체 문자열을 미리 생성하는 것이었습니다.
>>> a = ["hello", "world"]
>>> joined = '\n'.join(a)
>>> f"{joined}"
'hello\nworld'
이제 새로운 PEG 파서가 이를 쉽게 지원할 수 있으므로, 표현식 부분에서 백슬래시를 허용하는 것이 자연스럽게 느껴집니다.
>>> a = ["hello", "world"]
>>> f"{'\n'.join(a)}"
'hello\nworld'
이 문서에서 제안된 변경 사항 이전에는 f-string이 중첩될 수 있는 명시적인 제한이 없었지만, f-string의 표현식 구성 요소 내에서 문자열 인용 부호(string quotes)를 재사용할 수 없다는 사실 때문에 f-string을 임의로 중첩하는 것이 불가능했습니다. 실제로 작성할 수 있는 가장 깊게 중첩된 f-string은 다음과 같습니다.
>>> f"""{f'''{f'{f"{1+1}"}'}'''}"""
'2'
이 PEP는 f-string의 표현식 구성 요소 내부에 유효한 Python 표현식을 배치할 수 있도록 허용하므로, 이제 인용 부호를 재사용할 수 있으며, 따라서 f-string을 임의로 중첩할 수 있습니다.
>>> f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}"
'2'
비록 이것이 임의의 표현식을 허용하는 것의 결과일 뿐이지만, 이 PEP의 저자들은 이것이 근본적인 이점이라고 생각하지 않으며, 언어 명세서가 이러한 중첩이 임의적이어야 한다고 명시적으로 요구하지 않기로 결정했습니다. 이는 임의로 깊은 중첩을 허용하는 것이 렉서 구현에 많은 추가 복잡성을 부과하기 때문입니다(특히 렉서/파서 파이프라인이 ‘f-string 디버그 표현식’을 지원하기 위해 “토큰 해제(untokenizing)”를 허용해야 하며, 임의 중첩이 허용될 때 특히 부담이 큽니다). 따라서 구현체는 필요한 경우 중첩 깊이에 제한을 둘 수 있습니다. CPython 구현은 괄호 및 대괄호의 중첩 깊이, 블록의 중첩, if
문의 분기 수, 별표 언패킹(star-unpacking)의 표현식 수 등 여러 곳에서 이미 여러 제한을 부과하고 있으므로 이는 드문 상황이 아닙니다.
명세 (Specification)
f-string에 대해 제안된 공식 PEG 문법 명세는 다음과 같습니다(구문 세부 사항은 PEP 617 참조).
fstring
| FSTRING_START fstring_middle* FSTRING_END
fstring_middle
| fstring_replacement_field
| FSTRING_MIDDLE
fstring_replacement_field
| '{' (yield_expr | star_expressions) "="? [ "!" NAME ] [ ':' fstring_format_spec* ] '}'
fstring_format_spec:
| FSTRING_MIDDLE
| fstring_replacement_field
새로운 토큰(FSTRING_START
, FSTRING_MIDDLE
, FSTRING_END
)은 이 문서의 뒷부분에서 정의됩니다.
이 PEP는 f-string 중첩 수준(다른 f-string의 표현식 부분 내 f-string)을 구현에 맡기지만, 최소 5단계의 중첩을 지정합니다. 이는 사용자가 “합리적인” 깊이로 f-string을 중첩할 수 있다는 합리적인 기대를 가질 수 있도록 보장하기 위함입니다. 이 PEP는 중첩 제한이 언어 명세의 일부가 아님을 의미하지만, 언어 명세가 임의 중첩을 의무화하지도 않습니다.
마찬가지로, 이 PEP는 형식 지정자(format specifiers)의 표현식 중첩 수준을 구현에 맡기지만, 최소 2단계의 중첩을 지정합니다. 이는 다음이 항상 유효해야 함을 의미합니다.
f"{'':*^{1:{1}}}"
그러나 다음은 구현에 따라 유효할 수도 있고 아닐 수도 있습니다.
f"{'':*^{1:{1:{1}}}}"
새로운 문법은 현재 구현의 추상 구문 트리(Abstract Syntax Tree, AST)를 보존할 것입니다. 이는 이 PEP에 의해 기존 f-string 사용 코드에 어떠한 의미론적(semantic) 변경도 도입되지 않음을 의미합니다.
f-string 디버그 표현식 처리 (Handling of f-string debug expressions)
Python 3.8부터 f-string은 =
연산자를 사용하여 표현식을 디버깅하는 데 사용될 수 있습니다. 예를 들어:
>>> a = 1
>>> f"{1+1=}"
'1+1=2'
이러한 의미는 공식적으로 PEP에 도입되지 않았으며, 현재 문자열 파서에서 bpo-36817의 특별한 경우로 구현되었고, f-string 어휘 분석(lexical analysis) 섹션에 문서화되었습니다.
이 기능은 이 PEP에서 제안된 변경 사항의 영향을 받지 않지만, 이 기능을 공식적으로 처리하려면 렉서가 f-string의 표현식 부분을 “토큰 해제”할 수 있어야 한다는 점을 명시하는 것이 중요합니다. 이는 현재 문자열 파서가 문자열 토큰 내용에 직접 작동할 수 있으므로 문제가 되지 않습니다. 그러나 이 기능을 특정 파서 구현에 통합하려면 렉서가 f-string 표현식 부분의 원시 문자열 내용(raw string contents)을 추적하고 f-string 노드에 대한 파스 트리(parse tree)가 구성될 때 파서에서 사용할 수 있도록 해야 합니다. 순수한 “토큰 해제”만으로는 충분하지 않습니다. 현재 명세된 바와 같이 f-string 디버그 표현식은 표현식 내 공백을 보존하며, {
및 =
문자 뒤의 공백도 포함합니다. 이는 f-string 표현식 부분의 원시 문자열 내용이 손상되지 않고 보존되어야 하며, 단순히 관련 토큰만 보존되어서는 안 된다는 의미입니다.
파서/렉서 구현이 이 문제를 어떻게 처리할지는 물론 구현에 달려 있습니다.
새로운 토큰 (New tokens)
세 가지 새로운 토큰인 FSTRING_START
, FSTRING_MIDDLE
, FSTRING_END
가 도입됩니다. 다른 렉서는 특정 구현의 맥락에서 여기에 제안된 것보다 더 효율적인 다른 구현을 가질 수 있습니다. 그러나 다음 정의는 CPython의 공용 API(예: tokenize
모듈)의 일부로 사용되며, 독자가 제안된 문법 변경 사항 및 토큰 사용 방식을 더 잘 이해할 수 있도록 참조용으로도 제공됩니다.
FSTRING_START
: 이 토큰에는 f-string 접두사(f
/F
/fr
)와 여는 인용 부호(들)가 포함됩니다.FSTRING_MIDDLE
: 이 토큰에는 문자열 내부에서 표현식 부분이 아니고 여는 또는 닫는 중괄호가 아닌 텍스트 부분이 포함됩니다. 여기에는 여는 인용 부호와 첫 번째 표현식 중괄호({
) 사이의 텍스트, 두 표현식 중괄호(}
및{
) 사이의 텍스트, 마지막 표현식 중괄호(}
)와 닫는 인용 부호 사이의 텍스트가 포함될 수 있습니다.FSTRING_END
: 이 토큰에는 닫는 인용 부호가 포함됩니다.
이러한 토큰은 항상 문자열 부분이며, 지정된 제한이 있는 STRING
토큰과 의미론적으로 동일합니다. 이러한 토큰은 f-string을 렉싱(lexing)할 때 렉서에 의해 생성되어야 합니다. 즉, 토크나이저(tokenizer)는 더 이상 f-string에 대해 단일 토큰을 생성할 수 없습니다. 렉서가 이 토큰을 어떻게 방출하는지는 지정되지 않는데, 이는 모든 구현에 크게 의존하기 때문입니다(표준 라이브러리의 Python 버전 렉서조차도 PEG 파서에서 사용되는 것과 다르게 구현됩니다).
예를 들어:
f'some words {a+b:.3f} more words {c+d=} final words'
는 다음과 같이 토큰화됩니다.
FSTRING_START - "f'"
FSTRING_MIDDLE - 'some words '
LBRACE - '{'
NAME - 'a'
PLUS - '+'
NAME - 'b'
OP - ':'
FSTRING_MIDDLE - '.3f'
RBRACE - '}'
FSTRING_MIDDLE - ' more words '
LBRACE - '{'
NAME - 'c'
PLUS - '+'
NAME - 'd'
OP - '='
RBRACE - '}'
FSTRING_MIDDLE - ' final words'
FSTRING_END - "'"
한편, f"""some words"""
는 단순히 다음과 같이 토큰화됩니다.
FSTRING_START - 'f"""'
FSTRING_MIDDLE - 'some words'
FSTRING_END - '"""'
tokenize
모듈 변경 사항 (Changes to the tokenize
module)
tokenize
모듈은 f-string을 파싱할 때 이전 섹션에서 설명한 대로 이러한 토큰을 방출하도록 조정될 것입니다. 이를 통해 도구(tools)들은 이 새로운 토큰화 스키마를 활용하고 자체 f-string 토크나이저 및 파서를 구현할 필요가 없게 됩니다.
새로운 토큰 생성 방법 (How to produce these new tokens)
기존 렉서가 이러한 토큰을 방출하도록 조정하는 한 가지 방법은 “렉서 모드” 스택을 통합하거나 다른 렉서 스택을 사용하는 것입니다. 렉서는 f-string 시작 토큰을 만나면 “일반 Python 렉싱”에서 “f-string 렉싱”으로 전환해야 하며, f-string이 중첩될 수 있으므로 f-string이 닫힐 때까지 컨텍스트를 유지해야 하기 때문입니다. 또한 f-string 표현식 부분 내부의 “렉서 모드”는 일반 Python 렉서의 “상위 집합(super-set)”처럼 동작해야 합니다(표현식 부분의 }
종결자를 만나면 f-string 렉싱으로 다시 전환해야 할 뿐만 아니라 f-string 형식 지정 및 디버그 표현식도 처리해야 하기 때문입니다). 참조용으로, CPython과 유사한 토크나이저를 수정하여 이러한 새 토큰을 방출하는 알고리즘의 초안은 다음과 같습니다.
- 렉서가 f-string이 시작됨을 감지하면(
f
/F
문자 및 가능한 인용 부호 중 하나를 감지하여), 유효한 인용 부호("
,"""
,'
또는'''
중 하나)가 감지될 때까지 계속 진행하고, 캡처된 내용(f
/F
및 시작 인용 부호)으로FSTRING_START
토큰을 방출합니다. “F-string 토큰화”를 위한 새 토크나이저 모드를 토크나이저 모드 스택에 푸시합니다. 2단계로 이동합니다. - 다음 중 하나가 나타날 때까지 토큰을 계속 소비합니다.
- 여는 인용 부호와 일치하는 닫는 인용 부호.
- “형식 지정자 모드”인 경우(3단계 참조), 여는 중괄호(
{
), 닫는 중괄호(}
), 또는 개행 토큰(\n
). - “형식 지정자 모드”가 아닌 경우(3단계 참조), 다른 여는/닫는 중괄호가 즉시 뒤따르지 않는 여는 중괄호(
{
) 또는 닫는 중괄호(}
).
모든 경우에, 문자 버퍼가 비어 있지 않으면 지금까지 캡처된 내용으로
FSTRING_MIDDLE
토큰을 방출하되, 이중 여는/닫는 중괄호를 단일 여는/닫는 중괄호로 변환합니다. 이제 나타난 문자에 따라 다음과 같이 진행합니다.- 여는 인용 부호와 일치하는 닫는 인용 부호가 나타나면 4단계로 이동합니다.
- 여는 괄호(다른 여는 괄호가 즉시 뒤따르지 않는)가 나타나면 3단계로 이동합니다.
- 닫는 괄호(다른 닫는 괄호가 즉시 뒤따르지 않는)가 나타나면 닫는 괄호에 대한 토큰을 방출하고 2단계로 이동합니다.
- “f-string 내 일반 Python 토큰화”를 위한 새 토크나이저 모드를 토크나이저 모드 스택에 푸시하고 이를 사용하여 토큰화를 진행합니다. 이 모드는
f-string
부분으로 진입할 때 푸시되었던 여는 괄호 토큰과 동일한 중첩 수준으로:
또는}
문자가 나타날 때까지 “일반 Python 토큰화”처럼 토큰화합니다. 이 모드를 사용하여 중지 지점 중 하나에 도달할 때까지 토큰을 방출합니다. 이 경우, 중지 문자에 해당하는 토큰을 방출하고, 현재 토크나이저 모드를 토크나이저 모드 스택에서 팝(pop)하고 2단계로 이동합니다. 중지 지점이:
문자인 경우, “형식 지정자” 모드로 2단계에 진입합니다. - 캡처된 내용으로
FSTRING_END
토큰을 방출하고 현재 토크나이저 모드(“F-string 토큰화”에 해당)를 팝하고 “일반 Python 모드”로 돌아갑니다.
물론, 앞서 언급했듯이, 변경될 렉서의 특정 구현 및 특성에 따라 달라지므로 임의의 토크나이저에 대해 이것이 어떻게 수행되어야 하는지에 대한 정확한 명세를 제공하는 것은 불가능합니다.
새로운 문법의 결과 (Consequences of the new grammar)
아래에 설명된 대로, PEP에 언급된 모든 제한 사항이 f-string 리터럴에서 해제됩니다.
- 이제 표현식 부분에 f-string 리터럴을 구분하는 데 사용되는 것과 동일한 종류의 인용 부호로 구분된 문자열을 포함할 수 있습니다.
- 이제 Python 코드의 다른 모든 곳과 마찬가지로 표현식 내부에 백슬래시가 나타날 수 있습니다. f-string 리터럴 내부에 중첩된 문자열의 경우, 가장 안쪽 문자열이 평가될 때 이스케이프 시퀀스가 확장됩니다.
- 이제 표현식 중괄호 내부에 개행이 허용됩니다. 즉, 다음이 허용됩니다.
>>> x = 1 >>> f"___{ ... x ... }___" '___1___' >>> f"___{( ... x ... )}___" '___1___'
#
문자를 사용하는 주석은 f-string의 표현식 부분 내에서 허용됩니다. 주석은 표현식 부분의 닫는 괄호(}
)가 주석이 있는 줄과 다른 줄에 있어야 하며, 그렇지 않으면 주석의 일부로 무시된다는 점에 유의하십시오.
인용 부호 재사용 관련 고려 사항 (Considerations regarding quote reuse)
여기서 제안된 문법의 결과 중 하나는, 위에서 언급했듯이, f-string 표현식이 이제 외부 f-string 리터럴을 구분하는 데 사용되는 것과 동일한 종류의 인용 부호로 구분된 문자열을 포함할 수 있다는 것입니다. 예를 들어:
>>> f" something { my_dict["key"] } something else "
이 PEP에 대한 토론 스레드에서 이 측면에 대해 여러 우려가 제기되었으며, 이 PEP를 수락하거나 거부할 때 고려해야 할 사항이므로 여기에 수집하고자 합니다.
일부 반대 의견은 다음과 같습니다.
- 많은 사람들이 동일한 문자열 내에서 인용 부호 재사용을 혼란스럽고 읽기 어렵다고 생각합니다. 이는 인용 부호 재사용을 허용하면 현재 Python의 속성인 “문자열은 동일한 종류의 인용 부호 두 쌍으로 완전히 구분된다”는 매우 간단한 규칙을 위반하기 때문입니다. 인용 부호 재사용이 사람이 파싱하기 더 어려운 이유 중 하나는(다른 구분 기호와 달리) 인용 부호 문자가 시작과 끝이 동일하다는 점이며, 이는 코드 가독성을 저해할 수 있습니다.
- 일부 사용자들은 인용 부호 재사용이 정규 표현식이나 간단한 구분 기호 일치 도구와 같이 문자열 및 f-string을 감지하는 간단한 메커니즘에 의존하는 일부 렉서 및 구문 강조(syntax highlighting) 도구를 손상시킬 수 있다는 우려를 제기했습니다. f-string에 인용 부호 재사용을 도입하면 이러한 도구를 계속 작동시키기가 더 어려워지거나(예를 들어, 정규 표현식은 임의로 중첩된 구조를 구분 기호로 파싱할 수 없음) 도구가 완전히 작동하지 않게 될 것입니다. 표준 라이브러리에 포함된 IDLE 편집기는 f-string에 구문 강조를 올바르게 적용하기 위해 일부 작업이 필요할 수 있는 도구의 예입니다.
찬성하는 주장 중 일부는 다음과 같습니다.
- 유사한 구문 구조(일반적으로 “문자열 보간(string interpolation)”이라고 불림)를 허용하는 많은 언어는 인용 부호 재사용과 임의 중첩을 허용합니다. 이러한 언어에는 JavaScript, Ruby, C#, Bash, Swift 등이 포함됩니다. 많은 언어가 인용 부호 재사용을 허용한다는 사실은 Python에서 이를 허용하는 강력한 주장이 될 수 있습니다. 이는 다른 언어에서 온 사용자들에게 언어를 더 친숙하게 만들 것이기 때문입니다.
- 많은 다른 인기 있는 언어가 문자열 보간 구조에서 인용 부호 재사용을 허용하므로, 이러한 언어에 대한 구문 강조를 지원하는 편집기는 Python에서 인용 부호 재사용이 있는 f-string에 대한 구문 강조를 지원하는 데 필요한 도구를 이미 갖추고 있을 것입니다. 이는 Python에 대한 구문 강조를 처리하는 파일은 이 새로운 기능을 지원하기 위해 업데이트되어야 하지만, 불가능하거나 매우 어려운 작업은 아닐 것으로 예상됩니다.
-
인용 부호 재사용을 허용하는 한 가지 이점은 다른 구문과 깔끔하게 조합된다는 것입니다. 때로는 이를 “참조 투명성(referential transparency)”이라고 부르기도 합니다. 이에 대한 예는
f(x+1)
이 있다고 가정할 때,a
가 새로운 변수라면a = x+1; f(a)
와 동일하게 작동해야 한다는 것입니다. 그리고 그 반대도 마찬가지입니다. 따라서 다음과 같은 코드가 있을 때:def py2c(source): prefix = source.removesuffix(".py") return f"{prefix}.c"
변수
prefix
를 정의로 대체하면 결과가 동일해야 합니다.def py2c(source): return f"{source.removesuffix(".py")}.c"
- 현재 형태의 코드 생성기(예: 표준 라이브러리의
ast.unparse
)는 f-string 내의 표현식이 사용되는 컨텍스트에 적합하도록 복잡한 알고리즘에 의존합니다. 이러한 복잡한 알고리즘은 사용되지 않는 인용 부호 유형을 찾는 것(외부 인용 부호를 추적하여) 및 가능하면 백슬래시를 포함하지 않는 문자열 표현을 생성하는 것과 같은 어려움을 동반합니다. 인용 부호 재사용 및 백슬래시를 허용하면 f-string을 다루는 코드 생성기가 상당히 단순화될 것입니다. 왜냐하면 일반 Python 표현식 논리가 f-string 내부와 외부에서 특별한 처리 없이 사용될 수 있기 때문입니다. - 인용 부호 재사용을 제한하면 제안된 변경 사항의 구현 복잡성이 상당히 증가할 것입니다. 이는 파서가 주어진 인용 부호로 f-string의 표현식 부분을 파싱하는 컨텍스트를 가져야만 해당 인용 부호를 재사용하는 표현식을 거부해야 하는지 알 수 있도록 강제하기 때문입니다. 이러한 컨텍스트를 유지하는 것은 임의로 백트래킹(backtrack)할 수 있는 파서(예: PEG 파서)에서는 간단하지 않습니다. f-string이 임의로 중첩될 수 있으므로 여러 인용 부호 유형을 거부해야 할 수 있다는 점을 고려하면 문제는 훨씬 더 복잡해집니다.
커뮤니티로부터 피드백을 수집하기 위해, PEP의 이 측면에 대한 커뮤니티의 의견을 파악하기 위한 설문조사가 시작되었습니다.
하위 호환성 (Backwards Compatibility)
이 PEP는 Python 언어에 하위 호환성이 없는 구문적 또는 의미론적 변경 사항을 도입하지 않습니다. 그러나 tokenize
모듈(표준 라이브러리의 준공개 부분)은 새로운 f-string 토큰을 지원하기 위해 업데이트되어야 합니다(도구 작성자가 f-string을 올바르게 토큰화할 수 있도록). tokenize
의 공용 API가 어떻게 영향을 받을지에 대한 자세한 내용은 “Changes to the tokenize module” 섹션을 참조하십시오.
교육 방법 (How to Teach This)
f-string 개념은 이미 Python 커뮤니티에서 널리 사용되므로, 사용자가 새로 배울 근본적인 필요는 없습니다. 그러나 형식화된 문법이 몇 가지 새로운 가능성을 허용하므로, 이 PEP가 혼란을 피하는 것을 목표로 하기 때문에 공식 문법이 문서에 추가되고 자세히 설명되어 어떤 구조가 가능한지 명시적으로 언급하는 것이 중요합니다.
또한 사용자에게 f-string 표현식 내부에 무엇을 넣을 수 있는지 이해하기 위한 간단한 프레임워크를 제공하는 것이 유용합니다. 이 경우 저자들은 이 작업이 언어의 이 측면을 설명하는 것을 더욱 간단하게 만들 것이라고 생각합니다. 왜냐하면 다음과 같이 요약될 수 있기 때문입니다.
f-string 표현식 내부에 유효한 Python 표현식을 배치할 수 있습니다.
이 PEP의 변경 사항으로 인해, 문자열 인용 부호가 둘러싸는 문자열의 인용 부호와 달라야 한다고 명확히 할 필요가 없습니다. 왜냐하면 이제 이것이 허용되기 때문입니다. 임의의 Python 문자열이 가능한 모든 인용 부호 선택을 포함할 수 있듯이, 모든 f-string 표현식도 마찬가지입니다. 또한 주석, 개행 문자 또는 백슬래시와 같은 특정 사항이 구현 제한으로 인해 표현식 부분에서 허용되지 않는다고 명확히 할 필요도 없습니다.
유일하게 “놀라운” 차이점은 f-string이 형식을 지정할 수 있도록 허용하므로, 최상위 수준에서 :
문자를 허용하는 표현식은 여전히 괄호로 묶어야 한다는 것입니다. 이것은 새로운 내용은 아니지만, 이 제한이 여전히 유효하다는 점을 강조하는 것이 중요합니다. 이는 요약에 대한 더 쉬운 수정을 가능하게 합니다.
f-string 표현식 내부에 유효한 Python 표현식을 배치할 수 있으며, 최상위 수준의
:
문자 뒤의 모든 것은 형식 지정(format specification)으로 식별됩니다.
참조 구현 (Reference Implementation)
참조 구현은 implementation fork에서 찾을 수 있습니다.
거부된 아이디어 (Rejected Ideas)
f-string 표현식에서 인용 부호 재사용을 허용하는 것에 대해 제기된 가독성 주장이 유효하고 매우 중요하다고 생각하지만, 파서 수준에서 f-string의 인용 부호 재사용을 거부하지 않을 것을 제안하기로 결정했습니다. 그 이유는 이 PEP의 핵심 중 하나가 CPython에서 f-string 파싱의 복잡성과 유지보수를 줄이는 것인데, 이는 그 목표에 역행할 뿐만 아니라 현재 구현보다 더 복잡하게 만들 수도 있기 때문입니다. 우리는 인용 부호 재사용 금지는 린터(linters) 및 코드 스타일 도구에서 처리되어야 하며, 파서에서는 안 된다고 생각합니다. 이는 언어의 다른 혼란스럽거나 읽기 어려운 구조를 오늘날 처리하는 방식과 동일합니다.
일부 표현식 부분이 최상위 수준에서 :
및 !
를 괄호로 묶어야 한다는 제한을 해제하지 않기로 결정했습니다. 예를 들어:
>>> f'Useless use of lambdas: { lambda x: x*2 }'
SyntaxError: unexpected EOF while parsing
그 이유는 이것이 실제 이점 없이 상당한 양의 복잡성을 초래하기 때문입니다. 이는 :
문자가 일반적으로 f-string 형식 지정(format specification)을 구분하기 때문입니다. 이 형식 지정은 현재 문자열로 토큰화됩니다. 토크나이저는 :
의 오른쪽에 있는 것을 문자열 또는 토큰 스트림으로 토큰화해야 하므로(이는 토크나이저가 되돌아가서 다른 토큰 세트를 생성해야 함을 의미함), 파서가 다른 의미론을 구별할 수 없게 됩니다(즉, 먼저 토큰 스트림으로 시도하고, 실패하면 형식 지정자에 대한 문자열로 시도).
최상위 수준에서 람다(lambdas) 및 유사한 표현식을 허용하는 데 근본적인 이점이 없으므로, 필요한 경우 이러한 표현식을 괄호로 묶어야 한다는 제한을 유지하기로 결정했습니다.
>>> f'Useless use of lambdas: { (lambda x: x*2) }'
`` 구문 외에 이스케이프된 중괄호(\{
및 \}
) 사용을 (당분간) 허용하지 않기로 결정했습니다. PEP의 저자들은 이스케이프된 중괄호를 허용하는 것이 좋은 생각이라고 믿지만, 여기에서 제안된 f-string의 형식화에 엄격하게 필요하지 않으며 정규 CPython 문제에서 독립적으로 추가될 수 있으므로 이 PEP에 포함하지 않기로 결정했습니다.
미해결 문제 (Open Issues)
아직 없습니다.
저작권 (Copyright)
이 문서는 퍼블릭 도메인 또는 CC0-1.0-Universal 라이선스 중 더 관대한 라이선스에 따라 게시됩니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments