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

Python 개선 제안 한국어 번역

PEP 501 – 범용 템플릿 리터럴 문자열

Author:
Alyssa Coghlan <ncoghlan at gmail.com>, Nick Humrich <nick at humrich.us>
Discussions-To:
Discourse thread
Status:
Withdrawn
Type:
Standards Track
Requires:
701
Created:
08-Aug-2015
Python-Version:
3.12
Post-History:
08-Aug-2015, 05-Sep-2015, 09-Mar-2023
Superseded-By:
750

Table of Contents

번역·라이선스 안내

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

Important

This PEP has been superseded by PEP 750.

×

초록

Python f-string은 사용하기 쉽고 우아하지만, 셸 명령, SQL 쿼리, HTML 조각 등을 구성하는 데 사용하면 인젝션 공격에 취약할 수 있습니다(예: os.system(f"echo {message_from_user}")). 이 PEP는 템플릿 리터럴 문자열(또는 “t-문자열”)을 도입합니다. t-문자열은 f-문자열과 비슷한 문법과 의미를 가지지만, 렌더링은 format()이나 다른 템플릿 렌더링 함수가 호출될 때까지 지연됩니다. 이를 통해 표준 라이브러리 호출, 헬퍼 함수, 서드파티 도구가 f-문자열의 사용성과 편리함을 유지하면서도 입력에 대해 적절한 이스케이프와 기타 문자열 처리를 안전하고 지능적으로 수행할 수 있습니다.

PEP 철회

PEP 750이 (임의의 문자열 접두사를 허용하는) “태그된 문자열” 제안으로 처음 발표되었을 때, 이 PEP는 단일 전용 문자열 접두사를 사용하여 새로운 “보간 템플릿” 타입의 인스턴스를 생성하는 더 단순한 “템플릿 리터럴” 접근 방식을 계속 지지하기 위해 열린 상태로 유지되었습니다.

PEP 750의 2024년 10월 업데이트에서는 더 광범위한 태그된 문자열 개념보다 템플릿 문자열이 Python에 더 적합하다는 데 합의했습니다.

이 PEP의 저자들이 PEP 750에 대해 가졌던 다른 모든 우려 사항도 해당 업데이트에서 해결되었거나, 향후 변경 제안에서 합리적으로 해결할 수 있는 상태로 남겨졌습니다.

업데이트된 PEP 750 제안이 명확하게 개선되었으므로, 이 PEP는 PEP 750을 지지하며 철회되었습니다.

Important

이 PEP의 나머지 부분은 여전히 2024년 8월 시점의 태그된 문자열 제안 상태를 반영합니다. PEP가 철회되어 그렇게 하는 것이 불필요해졌으므로, PEP 750의 2024년 10월 변경 사항을 반영하도록 업데이트되지 않았습니다.

다른 PEP와의 관계

이 PEP는 PEP 498에서 처음 구현되고 PEP 701에서 공식화된 f-문자열 문법에서 영감을 받았으며 이를 기반으로 합니다.

이 PEP는 보안에 민감한 문자열에 런타임 값을 동적으로 보간하는 안전한 방법을 도입함으로써, PEP 675에서 Python의 공식 타입 시스템에 추가된 리터럴 문자열 타입 지원을 보완합니다.

이 PEP는 PEP 750의 태그된 문자열 제안과 일부 측면에서 경쟁하지만(가장 두드러진 것은 템플릿 렌더링을 render(t"template literal")로 표현하는지, render"template literal"로 표현하는지입니다), 많은 공통 기능도 공유합니다(PEP 750이 발표된 후, 이 PEP는 태그된 문자열 제안에서 영감을 받은 몇 가지 새로운 변경 사항으로 업데이트되었습니다).

이 PEP는 사용자 인터페이스 국제화 사용 사례를 위해 PEP 292를 대체할 방안을 제안하지 않습니다(다만 이 PEP와 PEP 750이 도입하는, 컴파일러가 지원하는 값 보간 메커니즘의 혜택을 받을 수 있는, 해당 사용 사례를 겨냥한 향후 문법 개선의 가능성은 언급합니다).

동기

PEP 498은 컴파일러에 투명한 문자열 보간에 대한 새로운 문법 지원을 추가하여, 보간 연산의 이름 참조가 명시적 이름 참조로 제한되지 않고 (다른 모든 표현식과 마찬가지로) 이를 포함하는 네임스페이스에 완전히 접근할 수 있도록 했습니다. 이들은 해당 PEP(및 다른 곳)에서 “f-문자열”(“formatted strings”를 떠올리게 하는 이름)이라고 불립니다.

PEP 498이 수락된 이후, f-문자열은 확고히 자리 잡았고 매우 널리 사용되고 있습니다. f-문자열은 PEP 701에서 공식화된 문법으로 더욱 유용하고 유연해졌습니다. f-문자열은 훌륭하지만, 즉시 렌더링에는 한계가 있습니다. 예를 들어, f-문자열의 즉시성 때문에 안타깝게도 다음과 같은 코드가 그럴듯해 보이게 되었습니다:

os.system(f"echo {message_from_user}")

이러한 종류의 코드는 겉보기에는 우아하지만, 보간된 값 message_from_user가 실제로 신뢰할 수 없는 사용자로부터 제공된 경우 심각한 문제를 일으킵니다. 제공된 사용자 데이터가 os.system 호출에 전달되기 전에 적절히 이스케이프되지 않으므로, 일종의 코드 인젝션 공격의 여지가 생기기 때문입니다.

PEP 675에서 도입된 LiteralString 타입 어노테이션 덕분에 타입 검사기가 이러한 종류의 안전하지 않은 함수 사용에 대해 타입 오류를 보고할 수 있지만, 이러한 오류는 더 안전한 대안(예: subprocess.run())을 사용하는 코드를 더 쉽게 작성하는 데는 도움이 되지 않습니다.

이 문제(그리고 그 밖의 여러 우려 사항)를 해결하기 위해, 이 PEP는 “t-문자열”(“템플릿 리터럴 문자열(template literal strings)”을 뜻하는 기억하기 쉬운 이름)을 보완적으로 도입할 것을 제안합니다. format(t"Message with {data}")는 f"Message with {data}"와 같은 결과를 내지만, 템플릿 리터럴 인스턴스는 템플릿의 내용을 다른 방식으로 처리하는 다른 템플릿 렌더링 함수에 대신 전달될 수 있습니다.

제안

전용 템플릿 리터럴 문법

이 PEP는 문자열이 일반 문자열이 아닌 템플릿 리터럴임을 선언하는 새로운 문자열 접두사를 제안합니다:

template = t"Substitute {names:>{field_width}} and {expressions()!r} at runtime"

이는 실질적으로 다음과 같이 해석됩니다:

template = TemplateLiteral(
    r"Substitute {names:>{field_width}} and {expressions()} at runtime",
    TemplateLiteralText(r"Substitute "),
    TemplateLiteralField("names", names, f">{field_width}", ""),
    TemplateLiteralText(r" and "),
    TemplateLiteralField("expressions()", expressions(), f"", "r"),
)

(참고: 이것은 예시를 위한 구현입니다. types.TemplateLiteral의 정확한 컴파일 타임 생성 문법은 PEP에서 규정하지 않는 구현 세부 사항으로 간주합니다. 특히 컴파일러는 연속된 텍스트 세그먼트를 감지해 하나의 텍스트 세그먼트로 병합하는 기본 생성자의 런타임 로직과, 제공된 모든 인자의 런타임 타입을 검사하는 로직을 우회할 수 있습니다).

그러면 types.TemplateLiteral의 __format__ 메서드는 str.format()에서 영감을 받은 다음 의미 체계를 구현하게 됩니다:

>>> import datetime
>>> name = 'Jane'
>>> age = 50
>>> anniversary = datetime.date(1991, 10, 12)
>>> format(t'My name is {name}, my age next year is {age+1}, my anniversary is {anniversary:%A, %B %d, %Y}.')
'My name is Jane, my age next year is 51, my anniversary is Saturday, October 12, 1991.'
>>> format(t'She said her name is {name!r}.')
"She said her name is 'Jane'."

템플릿 리터럴의 문법은 PEP 701을 기반으로 하며, 템플릿의 문자열 부분에는 대체로 같은 문법을 사용합니다. 다른 접두사를 사용한다는 점 외에 유일한 문법 변경은 변환 지정자의 정의와 처리 방식에 있습니다. 이는 렌더링 시점에 필드의 평가를 요청하는 표준 변환 지정자로 !()를 허용하고, 사용자 정의 렌더러도 사용자 정의 변환 지정자를 정의할 수 있게 하기 위함입니다.

이 PEP는 기존 문자열 포매팅 메커니즘 중 어느 것도 제거하거나 폐지하자고 제안하지 않습니다. 애플리케이션의 소스 코드에 직접 존재하지 않는 문자열을 포매팅할 때는 그 메커니즘들이 여전히 유용하기 때문입니다.

지연 필드 평가 변환 지정자

기존에 지원하던 a, r, s 변환 지정자에 더해, str.format(), str.format_map(), string.Formatter는 “보간된 값을 호출하라”는 의미의 변환 지정자로 ()를 받아들이도록 갱신됩니다.

사용자 정의 템플릿 렌더링 함수에서 표준 변환 지정자를 적용할 수 있도록 새로운 operator.convert_field() 함수가 추가됩니다.

내장 함수 format()의 시그니처와 동작도 세 번째 선택적 매개변수로 변환 지정자를 받도록 갱신됩니다. 비어 있지 않은 변환 지정자가 주어지면, __format__ 메서드를 조회하기 전에 operator.convert_field()로 값을 변환합니다.

사용자 정의 변환 지정자

기본 렌더러로도 여전히 템플릿을 포매팅할 수 있는 방식으로 필드별 추가 지시자를 사용자 정의 렌더링 함수에 전달할 수 있도록, 변환 지정자 필드에 두 번째 ! 문자를 포함하는 것을 허용합니다.

operator.convert_field()와 format() (그리고 그에 따라 기본 TemplateLiteral.render 템플릿 렌더링 메서드)은 변환 지정자 필드에서 그 문자와 그 뒤에 오는 모든 텍스트를 무시합니다.

str.format(), str.format_map(), string.Formatter도 사용자 정의 변환 지정자를 받아들이도록(그리고 무시하도록) 갱신됩니다.

POSIX 셸 명령을 위한 템플릿 렌더러

지연 렌더링 지원의 이점을 실제로 보여 주는 동시에 그 자체로도 가치 있는 기능으로서, 새로운 sh 템플릿 렌더러가 shlex 모듈에 추가됩니다. 이 렌더러는 보간된 모든 필드를 shlex.quote()로 이스케이프한 문자열을 생성합니다.

subprocess.Popen API(그리고 subprocess.run()처럼 이에 의존하는 상위 수준 API)는 보간 템플릿을 받아들이고 새로운 shlex.sh 렌더러에 따라 처리하도록 갱신됩니다.

배경

이 PEP는 처음에 PEP 498의 경쟁안으로 제안되었습니다. 즉시 렌더링 제안이 당장 훨씬 더 많은 지지를 받는다는 것이 분명해진 뒤, 이 PEP는 지연 렌더링까지 지원하는 추가 복잡성 없이 즉시 렌더링만 지원하는 PEP 498의 더 단순한 접근 방식에 대한 경험이 더 쌓이기를 기다리며 수년간 보류 상태로 있었습니다.

그 이후 f-문자열은 매우 인기를 얻었고, 그 문법과 의미 체계의 몇몇 거친 부분과 제약을 정리하기 위해 PEP 701이 도입되었습니다. 템플릿 리터럴 제안은 2023년에 f-문자열에 관한 현재의 지식과 PEP 701의 개선 사항을 반영하도록 갱신되었습니다.

2024년에는 이 PEP의 더 좁은 템플릿 리터럴 제안 대신, 사용자 정의 태그 문자열 접두사를 위한 범용 메커니즘을 제안하는 PEP 750이 발표되었습니다. 이 PEP는 태그 문자열 제안에서 영감을 받은 새로운 아이디어를 반영하고, 더 일반적인 태그 문자열 제안에 비해 이 PEP의 더 좁은 템플릿 리터럴 문법 제안이 갖는 것으로 보이는 이점을 설명하기 위해 다시 갱신되었습니다.

f-문자열과의 차이점 요약

f-문자열과 t-문자열의 주요 차이점은 다음과 같습니다:

  • t (템플릿 리터럴) 접두사는 지연 렌더링을 나타내지만, 그 밖에는 포맷 문자열과 대체로 같은 문법과 의미 체계를 사용합니다
  • 템플릿 리터럴은 런타임에 새로운 종류의 객체(types.TemplateLiteral)로 사용할 수 있습니다
  • 포맷 문자열이 사용하는 기본 렌더링은 컴파일된 코드에서 암묵적으로 수행되는 대신, format(template)을 호출하여 템플릿 리터럴 객체에 대해 실행됩니다
  • f-문자열(변환 지정자가 컴파일러에서 직접 처리됨)과 달리, t-문자열의 변환 지정자는 렌더링 시점에 렌더링 함수가 처리합니다.
  • 새로운 !() 변환 지정자는 필드 표현식이 호출 가능 객체이며 기본 format() 렌더링 함수를 사용할 때 호출되어야 함을 나타냅니다. 이 지정자는 f-문자열에는 의도적으로 추가되지 않습니다(그곳에서는 무의미하기 때문입니다).
  • 사용자 정의 템플릿 렌더링 함수가 기본 TemplateLiteral.render() 렌더링 메서드를 망가뜨리지 않고도 사용자 정의 변환 지정자를 받아들일 수 있도록, t-문자열 변환 지정자에는 두 번째 !가 허용됩니다(그 뒤에 오는 텍스트는 모두 무시됩니다). 이 기능은 f-문자열에는 의도적으로 추가되지 않습니다(그곳에서는 무의미하기 때문입니다).
  • f-문자열 f"Message {here}"는 format(t"Message {here}")와 의미상 동등하지만, f-문자열은 계속해서 컴파일러에서 직접 지원되므로 t-문자열에 필요한 지연 렌더링 메커니즘을 실제로 사용하는 데 따르는 런타임 오버헤드를 피합니다.

태그 문자열과의 차이점 요약

태그 문자열이 처음 제안되었을 때, 렌더링 함수 호출을 render(t"template literal")로 작성하는지 render"template literal"로 작성하는지라는 표면적인 문법 차이 외에도 PEP 501의 제안과 주목할 만한 차이점이 몇 가지 있었습니다.

초기 PEP 750 논의 과정에서 이러한 차이점 중 상당수가 해소되었습니다. PEP 501이 PEP 750 제안의 해당 측면을 채택하거나(변환 지정자를 지연 적용하는 것 등), PEP 750이 PEP 501 제안의 일부 측면을 유지하도록 변경되었기 때문입니다(템플릿 세그먼트를 단순한 시퀀스로 표현하는 대신 이를 담는 전용 타입을 정의하는 것 등).

남아 있는 주요한 차이점은, 이 PEP는 오직 t-문자열 접두사만 추가하는 것으로도 PEP 750에서 설명한 바람직한 이점을 모두 얻기에 충분한 개선이라고 주장한다는 점입니다. 일반화된 “태그 문자열” 문법으로 확장할 필요는 없으며, 그렇게 확장하면 피할 수 있는 추가적인 문제가 생깁니다.

두 PEP는 템플릿 필드의 지연 평가를 처리하기 위해 제안하는 접근 방식에서도 차이가 있습니다.

두 제안 사이에는 다른 차이점도 있기는 하지만, 그 차이점은 본질적이라기보다는 표면적인 것입니다. 구체적으로는 다음과 같습니다.

  • 이 PEP는 구조적 타이핑 프로토콜에 대해 다른 이름을 제안합니다
  • 이 PEP는 구체적인 구현 타입에 대해 특정 이름을 제안합니다
  • 이 PEP는 구체적인 구현 타입이 제공할 API의 정확한 세부 사항을 제안합니다(구조적 타이핑 프로토콜에 포함되지 않는 연결 및 반복 지원도 포함합니다)
  • 이 PEP는 기존 format() 내장 함수를 템플릿 필드 렌더러로 직접 사용할 수 있도록 변경할 것을 제안합니다

두 PEP는 지연 렌더링 지원의 필요성을 어떻게 주장하는지에서도 차이가 있습니다. 이 PEP는 템플릿 리터럴을 사용하여 f-문자열 처리의 “보간” 단계와 “렌더링” 단계를 시간적으로 분리할 수 있게 하고, 이를 활용하여 f-문자열의 오용과 관련된 잠재적인 코드 주입 위험을 줄인다는 구체적인 구현 개념에 더 초점을 맞춥니다. PEP 750은 네이티브 템플릿 지원이 기존의 문자열 기반 템플릿 방식으로는 달성하기 어렵거나 불가능한 동작을 가능하게 한다는 점에 더 초점을 맞춥니다. 앞서 언급한 표면적인 차이점과 마찬가지로, 이는 본질의 차이라기보다는 스타일의 차이입니다.

근거

f-문자열(PEP 498)은 Python의 렉시컬 네임스페이스 의미에 완전히 접근하면서 문자열에 값을 보간하는 일을 더 간단하게 만들었지만, 그 대가로 SQL 쿼리, 셸 명령, HTML 템플릿처럼 민감한 대상에 값을 보간할 때 코드 주입 공격을 고려하지 않고 처리하는 쪽이 올바르게 처리하는 쪽보다 훨씬 깔끔한 문법을 누리게 되는 상황을 만들었습니다.

이 PEP는 템플릿 리터럴을 서식화된 문자열로 실제 렌더링하는 작업을 템플릿 리터럴의 __format__ 메서드로 미룰 수 있는 선택지를 제공하여, 템플릿을 일급 객체로 전달함으로써 다른 템플릿 렌더러를 사용할 수 있도록 할 것을 제안합니다.

기술적인 세부 사항은 매우 다르지만, 이 PEP에서 제안하는 types.TemplateLiteral 인터페이스는 개념적으로 C# 6.0에서 도입된 네이티브 보간 지원의 기반이 되는 FormattableString 타입, 그리고 ES6에서 도입된 JavaScript 템플릿 리터럴과 상당히 유사합니다.

이 제안을 개발한 원래 동기는 아니었지만, PEP 750에서 설명한 도메인 특화 언어 정의의 여러 이점이 이 PEP에도 적용됩니다(선언된 템플릿 변수와 렌더링 함수 매개변수의 타입 명세를 기반으로 코드 편집기에서 DSL별 의미론적 하이라이팅을 제공할 가능성도 포함합니다).

명세

이 PEP는 새로운 타입인 types.TemplateLiteral의 인스턴스를 생성하는 새로운 t 문자열 접두사를 제안합니다.

템플릿 리터럴은 유니코드 문자열이며(바이트열 리터럴은 허용되지 않습니다), 문자열 리터럴 연결은 평소와 같이 동작하여 결합된 리터럴 전체가 템플릿 리터럴을 이룹니다.

템플릿 문자열은 PEP 498과 PEP 701에서 f-문자열에 대해 설명한 대로 리터럴, 표현식, 형식 지정자, 변환 지정자로 파싱됩니다. 변환 지정자의 문법은 유효한 Python 식별자로 제한되는 대신 임의의 문자열({, } 또는 :를 포함하는 문자열은 제외)을 허용하도록 완화됩니다.

그러나 이 구성 요소들은 서식이 적용된 문자열로 바로 렌더링되지 않습니다. 대신 다음과 같이 동작하는 새로운 타입의 인스턴스로 구성됩니다:

class TemplateLiteralText(str):
    # This is a renamed and extended version of the DecodedConcrete type in PEP 750
    # Real type would be implemented in C, this is an API compatible Python equivalent
    _raw: str

    def __new__(cls, raw: str):
        decoded = raw.encode("utf-8").decode("unicode-escape")
        if decoded == raw:
            decoded = raw
        text = super().__new__(cls, decoded)
        text._raw = raw
        return text

    @staticmethod
    def merge(text_segments:Sequence[TemplateLiteralText]) -> TemplateLiteralText:
        if len(text_segments) == 1:
            return text_segments[0]
        return TemplateLiteralText("".join(t._raw for t in text_segments))

    @property
    def raw(self) -> str:
        return self._raw

    def __repr__(self) -> str:
        return f"{type(self).__name__}(r{self._raw!r})"

    def __add__(self, other:Any) -> TemplateLiteralText|NotImplemented:
        if isinstance(other, TemplateLiteralText):
            return TemplateLiteralText(self._raw + other._raw)
        return NotImplemented


    def __mul__(self, other:Any) -> TemplateLiteralText|NotImplemented:
        try:
            factor = operator.index(other)
        except TypeError:
            return NotImplemented
        return TemplateLiteralText(self._raw * factor)
    __rmul__ = __mul__

class TemplateLiteralField(NamedTuple):
    # This is mostly a renamed version of the InterpolationConcrete type in PEP 750
    # However:
    #    - value is eagerly evaluated (values were all originally lazy in PEP 750)
    #    - conversion specifiers are allowed to be arbitrary strings
    #    - order of fields is adjusted so the text form is the first field and the
    #      remaining parameters match the updated signature of the `*format` builtin
    # Real type would be implemented in C, this is an API compatible Python equivalent

    expr: str
    value: Any
    format_spec: str | None = None
    conversion_spec: str | None = None

    def __repr__(self) -> str:
        return (f"{type(self).__name__}({self.expr}, {self.value!r}, "
                f"{self.format_spec!r}, {self.conversion_spec!r})")

    def __str__(self) -> str:
        return format(self.value, self.format_spec, self.conversion_spec)

    def __format__(self, format_override) -> str:
        if format_override:
            format_spec = format_override
        else:
            format_spec = self.format_spec
        return format(self.value, format_spec, self.conversion_spec)

class TemplateLiteral:
    # This type corresponds to the TemplateConcrete type in PEP 750
    # Real type would be implemented in C, this is an API compatible Python equivalent
    _raw_template: str
    _segments = tuple[TemplateLiteralText|TemplateLiteralField]

    def __new__(cls, raw_template:str, *segments:TemplateLiteralText|TemplateLiteralField):
        self = super().__new__(cls)
        self._raw_template = raw_template
        # Check if there are any adjacent text segments that need merging
        # or any empty text segments that need discarding
        type_err = "Template literal segments must be template literal text or field instances"
        text_expected = True
        needs_merge = False
        for segment in segments:
            match segment:
                case TemplateLiteralText():
                    if not text_expected or not segment:
                        needs_merge = True
                        break
                    text_expected = False
                case TemplateLiteralField():
                    text_expected = True
                case _:
                    raise TypeError(type_err)
        if not needs_merge:
            # Match loop above will have checked all segments
            self._segments = segments
            return self
        # Merge consecutive runs of text fields and drop any empty text fields
        merged_segments:list[TemplateLiteralText|TemplateLiteralField] = []
        pending_merge:list[TemplateLiteralText] = []
        for segment in segments:
            match segment:
                case TemplateLiteralText() as text_segment:
                    if text_segment:
                        pending_merge.append(text_segment)
                case TemplateLiteralField():
                    if pending_merge:
                        merged_segments.append(TemplateLiteralText.merge(pending_merge))
                        pending_merge.clear()
                    merged_segments.append(segment)
                case _:
                    # First loop above may not check all segments when a merge is needed
                    raise TypeError(type_err)
        if pending_merge:
            merged_segments.append(TemplateLiteralText.merge(pending_merge))
            pending_merge.clear()
        self._segments = tuple(merged_segments)
        return self

    @property
    def raw_template(self) -> str:
        return self._raw_template

    @property
    def segments(self) -> tuple[TemplateLiteralText|TemplateLiteralField]:
        return self._segments

    def __len__(self) -> int:
        return len(self._segments)

    def __iter__(self) -> Iterable[TemplateLiteralText|TemplateLiteralField]:
        return iter(self._segments)

    # Note: template literals do NOT define any relative ordering
    def __eq__(self, other):
        if not isinstance(other, TemplateLiteral):
            return NotImplemented
        return (
            self._raw_template == other._raw_template
            and self._segments == other._segments
            and self.field_values == other.field_values
            and self.format_specifiers == other.format_specifiers
        )

    def __repr__(self) -> str:
        return (f"{type(self).__name__}(r{self._raw!r}, "
                f"{', '.join(map(repr, self._segments))})")

    def __format__(self, format_specifier) -> str:
        # When formatted, render to a string, and then use string formatting
        return format(self.render(), format_specifier)

    def render(self, *, render_template=''.join, render_text=str, render_field=format):
        ...  # See definition of the template rendering semantics below

    def __add__(self, other) -> TemplateLiteral|NotImplemented:
        if isinstance(other, TemplateLiteral):
            combined_raw_text = self._raw + other._raw
            combined_segments = self._segments + other._segments
            return TemplateLiteral(combined_raw_text, *combined_segments)
        if isinstance(other, str):
            # Treat the given string as a new raw text segment
            combined_raw_text = self._raw + other
            combined_segments = self._segments + (TemplateLiteralText(other),)
            return TemplateLiteral(combined_raw_text, *combined_segments)
        return NotImplemented

    def __radd__(self, other) -> TemplateLiteral|NotImplemented:
        if isinstance(other, str):
            # Treat the given string as a new raw text segment. This effectively
            # has precedence over string concatenation in CPython due to
            # https://github.com/python/cpython/issues/55686
            combined_raw_text = other + self._raw
            combined_segments = (TemplateLiteralText(other),) + self._segments
            return TemplateLiteral(combined_raw_text, *combined_segments)
        return NotImplemented

    def __mul__(self, other) -> TemplateLiteral|NotImplemented:
        try:
            factor = operator.index(other)
        except TypeError:
            return NotImplemented
        if not self or factor == 1:
            return self
        if factor < 1:
            return TemplateLiteral("")
        repeated_text = self._raw_template * factor
        repeated_segments = self._segments * factor
        return TemplateLiteral(repeated_text, *repeated_segments)
    __rmul__ = __mul__

(참고: 이것은 설명을 위한 예시 구현입니다. types.TemplateLiteral의 정확한 컴파일 시점 생성 방법과 내부 데이터 관리 세부 사항은 구현 세부 사항으로 간주되며, 이 PEP에서 명시하지 않습니다. 그러나 생성 이후 types.TemplateLiteral 인스턴스의 공개 API가 보여야 할 동작은 위 코드에서 명시합니다. 런타임에 템플릿 인스턴스를 만드는 생성자 시그니처도 마찬가지로 위 코드에서 명시합니다)

템플릿 리터럴 표현식의 결과는 이미 렌더링된 문자열이 아니라 이 타입의 인스턴스입니다. 렌더링은 인스턴스의 render 메서드가 호출될 때만 일어납니다(직접 호출되든, __format__을 통해 간접적으로 호출되든 마찬가지입니다).

컴파일러는 나중에 사용할 수 있도록 다음 정보를 템플릿 리터럴에 전달합니다:

  • 소스 코드에 작성된 그대로의 원시 템플릿을 담은 문자열
  • 템플릿 세그먼트의 시퀀스. 각 세그먼트는 다음 중 하나입니다:
    • 리터럴 텍스트 세그먼트(원시 형태에도 접근할 수 있게 해 주는 일반 Python 문자열)
    • 파싱된 템플릿 보간 필드로, 보간된 표현식의 텍스트(일반 문자열), 그 평가 결과, 형식 지정자 텍스트(모든 치환 필드는 f-문자열로서 즉시 평가됨), 변환 지정자 텍스트(일반 문자열)를 지정합니다.

원시 템플릿은 템플릿 리터럴을 그대로 문자열로 나타낸 것일 뿐입니다. 기본적으로는 템플릿 리터럴을 사람이 읽을 수 있는 형태로 표현하는 데 사용되지만, 템플릿 렌더러가 다른 용도(예: 캐시 조회 키)로 사용할 수도 있습니다.

파싱된 템플릿 구조는 PEP 750에서 가져온 것으로, 템플릿 문자열의 텍스트 세그먼트와 보간 필드에 대응하는 템플릿 세그먼트의 시퀀스로 이루어집니다.

이 방식은 컴파일러가 템플릿의 각 세그먼트를 순서대로 완전히 처리한 다음, 마지막에 모든 템플릿 세그먼트를 템플릿 리터럴 생성자에 전달하는 코드를 생성할 수 있도록 설계되었습니다.

예를 들어, 런타임 값이 다음과 같다고 가정합니다:

names = ["Alice", "Bob", "Carol", "Eve"]
field_width = 10
def expressions():
    return 42

제안 절의 템플릿은 런타임에 다음과 같이 표현됩니다:

TemplateLiteral(
    r"Substitute {names:>{field_width}} and {expressions()!r} at runtime",
    TemplateLiteralText(r"Substitute "),
    TemplateLiteralField("names", ["Alice", "Bob", "Carol", "Eve"], ">10", ""),
    TemplateLiteralText(r" and "),
    TemplateLiteralField("expressions()", 42, "", "r"),
)

템플릿 렌더링

TemplateLiteral.render 구현은 다음 렌더러들을 사용해 렌더링 과정을 정의합니다:

  • 렌더링된 텍스트 세그먼트와 필드 세그먼트의 시퀀스를 조합해 완전히 렌더링된 결과를 만드는 방법을 정의하는 전체 render_template 연산. 기본 템플릿 렌더러는 ''.join을 사용한 문자열 연결입니다.
  • 템플릿 안의 개별 리터럴 텍스트 세그먼트를 받는 텍스트 세그먼트별 render_text 연산. 기본 텍스트 렌더러는 내장 str 생성자입니다.
  • 템플릿 내 치환 필드에 대한 필드 값, 형식 지정자, 변환 지정자를 받는 필드 세그먼트별 render_field 연산입니다. 기본 필드 렌더러는 내장 함수 format()입니다.

위와 같이 파싱된 템플릿 표현이 주어지면, 템플릿 렌더링의 의미는 다음과 동등합니다:

def render(self, *, render_template=''.join, render_text=str, render_field=format):
    rendered_segments = []
    for segment in self._segments:
        match segment:
            case TemplateLiteralText() as text_segment:
                rendered_segments.append(render_text(text_segment))
            case TemplateLiteralField() as field_segment:
                rendered_segments.append(render_field(*field_segment[1:]))
    return render_template(rendered_segments)

형식 지정자

t-문자열에서 필드 지정자의 문법과 처리 방식은 f-문자열과 같게 정의됩니다.

여기에는 필드 지정자 자체에 f-문자열 치환 필드를 넣을 수 있다는 점도 포함됩니다. 필드 지정자의 원시 텍스트(치환 필드를 처리하지 않은 상태)는 전체 원시 템플릿 문자열의 일부로 유지됩니다.

파싱된 필드 지정자는 해당 치환이 이미 해석된 필드 지정자 문자열을 받습니다. : 접두사도 생략됩니다.

파싱 중에 치환 표현식에서 분리된다는 점을 제외하면, 형식 지정자는 보간 템플릿 파서에서 불투명한 문자열로 취급됩니다. 형식 지정자에 의미를 부여하는 일(또는 그 사용을 금지하는 일)은 렌더링 시점에 필드 렌더러가 처리합니다.

변환 지정자

기존의 a, r, s 변환 지정자 지원에 더해, str.format()과 str.format_map()은 “보간된 값을 호출한다”는 의미의 변환 지정자로 ()를 받아들이도록 갱신됩니다.

PEP 701은 변환 지정자를 NAME 토큰으로 제한하지만, 이 PEP는 대신 FSTRING_MIDDLE 토큰을 허용합니다(따라서 {, }, :만 허용되지 않습니다). 이 변경은 주로 !() 변환 지정자를 사용한 지연 필드 렌더링을 지원하기 위한 것이지만, 사용자 정의 렌더링 함수가 기본 format() 필드 렌더러에 정의된 변환 지정자 대신 자체 변환 지정자를 정의할 때 더 많은 유연성을 제공하기도 합니다.

변환 지정자는 여전히 일반 문자열로 처리되며, 치환 필드는 사용할 수 없습니다.

파싱된 변환 지정자는 ! 접두사가 생략된 변환 지정자 문자열을 받습니다.

사용자 정의 템플릿 렌더러가 기본 렌더러를 실패하게 만들지 않으면서 자체 사용자 정의 변환 지정자를 정의할 수 있도록, 변환 지정자에는 두 번째 ! 문자를 접두사로 하는 사용자 정의 접미사를 포함하는 것이 허용됩니다. 즉, !!<custom>, !a!<custom>, !r!<custom>, !s!<custom>, !()!<custom>은 모두 템플릿 리터럴에서 유효한 변환 지정자입니다.

앞서 설명한 것처럼, 기본 렌더링은 PEP 3101에 정의된 기존 !a, !r, !s 변환 지정자와 함께 이 PEP에서 정의하는 새로운 !() 지연 필드 평가 변환 지정자를 지원합니다. 기본 렌더링은 사용자 정의 변환 지정자 접미사를 모두 무시합니다.

표준 변환 지정자와, 필드가 렌더링될 때 보간된 값에 대해 호출되는 특수 메서드 사이의 전체 매핑은 다음과 같습니다:

  • 변환 없음(빈 문자열): __format__ (형식 지정자를 매개변수로 사용)
  • a: __repr__ (ascii() 내장 함수와 같음)
  • r: __repr__ (repr() 내장 함수와 같음)
  • s: __str__ (str 내장 함수와 같음)
  • (): __call__ (매개변수 없음)

변환이 일어나면, __format__이 (형식 지정자와 함께) 원래 객체가 아닌 변환 결과에 대해 호출됩니다.

format()의 변경과 operator.convert_field()의 추가로 사용자 정의 렌더러도 표준 변환 지정자를 쉽게 지원할 수 있습니다.

f-문자열 자체는 새로운 !() 변환 지정자를 지원하지 않습니다(값 보간과 값 렌더링이 항상 동시에 일어나므로 불필요하기 때문입니다). 또한 f-문자열은 사용자 정의 변환 지정자도 지원하지 않습니다(렌더링 함수가 컴파일 시점에 이미 정해져 있고 사용자 정의 지정자를 사용하지 않기 때문입니다).

operator 모듈의 새로운 필드 변환 API

사용자 정의 템플릿 렌더링 함수에서 표준 변환 지정자를 적용할 수 있도록, 새로운 operator.convert_field() 함수가 추가됩니다:

def convert_field(value, conversion_spec=''):
    """Apply the given string formatting conversion specifier to the given value"""
    std_spec, sep, custom_spec = conversion_spec.partition("!")
    match std_spec:
        case '':
            return value
        case 'a':
            return ascii(value)
        case 'r':
            return repr(value)
        case 's':
            return str(value)
        case '()':
            return value()
    if not sep:
        err = f"Invalid conversion specifier {std_spec!r}"
    else:
        err = f"Invalid conversion specifier {std_spec!r} in {conversion_spec!r}"
    raise ValueError(f"{err}: expected '', 'a', 'r', 's' or '()')

format()에 추가된 변환 지정자 매개변수

format() 내장 함수의 시그니처와 동작이 다음과 같이 갱신됩니다:

def format(value, format_spec='', conversion_spec=''):
    if conversion_spec:
        value_to_format = operator.convert_field(value)
    else:
        value_to_format = value
    return type(value_to_format).__format__(value, format_spec)

비어 있지 않은 변환 지정자가 주어지면, __format__ 메서드를 조회하기 전에 operator.convert_field()로 값이 변환됩니다.

__format__ 특수 메서드의 시그니처는 변경되지 않습니다(형식화되는 객체는 형식 지정자만 처리합니다).

구조적 타이핑과 덕 타이핑

사용자 정의 렌더러가 (네이티브 템플릿 리터럴 타입에 강하게 결합되는 대신) 대체 보간 템플릿 구현을 받아들일 수 있도록, 다음 구조적 프로토콜이 typing 모듈에 추가됩니다:

@runtime_checkable
class TemplateText(Protocol):
    # Renamed version of PEP 750's Decoded protocol
    def __str__(self) -> str:
        ...

    raw: str

@runtime_checkable
class TemplateField(Protocol):
    # Renamed and modified version of PEP 750's Interpolation protocol
    def __len__(self):
        ...

    def __getitem__(self, index: int):
        ...

    def __str__(self) -> str:
        ...

    expr: str
    value: Any
    format_spec: str | None = None
    conversion_spec: str | None = None

@runtime_checkable
class InterpolationTemplate(Protocol):
    # Corresponds to PEP 750's Template protocol
    def __iter__(self) -> Iterable[TemplateText|TemplateField]:
        ...

    raw_template: str

구조적 프로토콜 API는 TemplateLiteralText, TemplateLiteralField, TemplateLiteral에 정의된 전체 구현 API보다 범위가 상당히 좁다는 점에 유의하십시오.

보간 템플릿을 받아들여 이에 대한 특정 처리를 정의하되, typing 모듈에 대한 의존성을 도입하거나 구체적인 템플릿 리터럴 타입만 처리하도록 코드를 제한하지 않으려는 코드는 대신 raw_template 속성이 존재하는지 검사해야 합니다.

사용자 정의 렌더러 작성

사용자 정의 렌더러를 작성하는 데 특별한 문법은 필요하지 않습니다. 대신, 사용자 정의 렌더러는 대체 render_template, render_text, render_field 구현 중 일부 또는 전부를 지정하여 render() 메서드를 호출하거나, 템플릿의 데이터 속성에 직접 접근하는 방식으로 보간 템플릿을 직접 처리하는 평범한 호출 가능 객체입니다.

예를 들어, 다음 함수는 객체 고유의 포매팅 지원 대신 객체의 repr 구현을 사용하여 템플릿을 렌더링합니다:

def repr_format(template):
    def render_field(value, format_spec, conversion_spec):
        converted_value = operator.convert_field(value, conversion_spec)
        return format(repr(converted_value), format_spec)
    return template.render(render_field=render_field)

위에서 보인 사용자 정의 렌더러는 원래 템플릿의 변환 지정자를 따르지만, 이를 무시하고 보간된 값을 직접 렌더링할 수도 있습니다:

def input_repr_format(template):
    def render_field(value, format_spec, __):
        return format(repr(value), format_spec)
    return template.render(render_field=render_field)

사용자 정의 렌더러를 작성할 때, 전체 렌더링 작업의 반환 타입은 전달된 render_template 호출 가능 객체의 반환 타입에 따라 결정된다는 점에 유의하십시오. 포매팅 관련 사용 사례에서는 여전히 문자열이겠지만, 문자열이 아닌 객체를 생성하는 것도 허용됩니다. 예를 들어, 사용자 정의 SQL 템플릿 렌더러는 SQL Alchemy 쿼리 객체를 생성하는 sqlalchemy.sql.text 호출을 사용할 수 있습니다. 서브프로세스 호출 관련 템플릿 렌더러는 subprocess.run에 전달하기에 적합한 문자열 시퀀스를 생성할 수도 있고, 심지어 subprocess.run을 직접 호출하고 그 결과를 반환할 수도 있습니다.

그러한 동작을 예상하는 render_template 구현과 짝을 이루기만 한다면, render_text와 render_field에서도 문자열이 아닌 값을 반환할 수 있습니다.

PEP 750에서 설명하는 패턴 매칭 스타일을 사용하는 사용자 정의 렌더러도 지원됩니다:

# Use the structural typing protocols rather than the concrete implementation types
from typing import InterpolationTemplate, TemplateText, TemplateField

def greet(template: InterpolationTemplate) -> str:
    """Render an interpolation template using structural pattern matching."""
    result = []
    for segment in template:
        match segment:
            match segment:
                case TemplateText() as text_segment:
                    result.append(text_segment)
                case TemplateField() as field_segment:
                    result.append(str(field_segment).upper())
    return f"{''.join(result)}!"

표현식 평가

f-문자열과 마찬가지로, 보간 템플릿에서 추출된 하위 표현식은 템플릿 리터럴이 나타나는 컨텍스트에서 평가됩니다. 이는 표현식이 지역, 비지역, 전역 변수에 완전히 접근할 수 있다는 뜻입니다. 함수 호출과 메서드 호출을 포함하여 유효한 Python 표현식이라면 무엇이든 {} 안에서 사용할 수 있습니다.

치환 표현식은 소스 코드에서 문자열이 나타나는 위치에서 평가되므로, 표현식 내용 자체와 관련된 추가적인 보안 문제는 없습니다. 같은 표현식을 직접 작성하고 런타임 필드 파싱을 사용할 수도 있었기 때문입니다:

>>> bar=10
>>> def foo(data):
...   return data + 20
...
>>> str(t'input={bar}, output={foo(bar)}')
'input=10, output=30'

이는 본질적으로 다음과 동등합니다:

>>> 'input={}, output={}'.format(bar, foo(bar))
'input=10, output=30'

코드 주입 공격 처리

PEP 498 형식 문자열 문법 때문에 다음과 같은 코드를 작성하고 싶어질 수 있습니다:

runquery(f"SELECT {column} FROM {table};")
runcommand(f"cat {filename}")
return_response(f"<html><body>{response.body}</body></html>")

보간되는 변수 중 하나라도 신뢰할 수 없는 출처에서 온 것이라면, 이 코드들은 모두 코드 주입 공격의 잠재적인 경로가 됩니다. 이 PEP의 구체적인 제안은 관련 보안 컨텍스트에 맞게 보간된 값을 적절히 인용 처리하는 사용 사례별 렌더러를 쉽게 작성할 수 있도록 설계되었습니다:

runquery(sql(t"SELECT {column} FROM {table} WHERE column={value};"))
runcommand(sh(t"cat {filename}"))
return_response(html(t"<html><body>{response.body}</body></html>"))

이 PEP는 그러한 렌더러를 모두 표준 라이브러리에 즉시 추가하는 것을 다루지는 않습니다(셸 이스케이프용 렌더러 하나는 제안합니다). 대신 서드파티 라이브러리가 그러한 렌더러를 쉽게 제공할 수 있도록 하고, 나중에 표준 라이브러리에 포함될 수도 있게 하는 것을 제안합니다.

시간이 지나면 잠재적으로 위험한 문자열 입력을 처리하는 API가 보간 템플릿을 기본적으로 받아들이도록 갱신될 것으로 예상됩니다. 그러면 f 문자열 접두사를 t로 바꾸는 것만으로 문제가 있는 코드 예제를 고칠 수 있습니다:

runquery(t"SELECT {column} FROM {table};")
runcommand(t"cat {filename}")
return_response(t"<html><body>{response.body}</body></html>")

shlex 모듈에 렌더러를 포함할 것을 제안합니다. 이 렌더러는 os.system을 실행하거나 subprocess 모듈 API를 사용할 때 시스템 셸을 활성화하는 데 따르는 상당한 위험 없이, 외부 프로그램에 접근하는 데 더 POSIX 셸다운 경험을 제공하는 것을 목표로 합니다. 이 렌더러는 Julia 프로그래밍 언어가 제공하는 인터페이스에서 영감을 받은 외부 프로그램 실행 인터페이스를 제공합니다. 다만 백틱 기반의 \`cat $filename\` 문법 대신 t"cat {filename}" 스타일의 템플릿 리터럴을 사용합니다. 자세한 내용은 shlex에 셸 이스케이프용 렌더러 추가 절을 참조하십시오.

오류 처리

보간 표현식을 처리할 때 컴파일 시간 오류나 런타임 오류가 발생할 수 있습니다. 컴파일 시간 오류는 템플릿 문자열을 구성 요소 튜플로 파싱할 때 감지할 수 있는 오류로 한정됩니다. 이러한 오류는 모두 SyntaxError를 발생시킵니다.

짝이 맞지 않는 중괄호:

>>> t'x={x'
  File "<stdin>", line 1
      t'x={x'
         ^
SyntaxError: missing '}' in template literal expression

유효하지 않은 표현식:

>>> t'x={!x}'
  File "<fstring>", line 1
    !x
    ^
SyntaxError: invalid syntax

런타임 오류는 템플릿 리터럴 객체를 생성하기 전에 템플릿 문자열 안의 표현식을 평가할 때 발생합니다. 몇 가지 예는 PEP 498을 참조하십시오.

렌더러에 따라 허용되는 보간 표현식과 기타 포매팅 세부 사항에 추가적인 런타임 제약을 둘 수도 있으며, 이는 런타임 예외로 보고됩니다.

shlex에 셸 이스케이프용 렌더러 추가

참조 구현으로서, 안전한 POSIX 셸 이스케이프를 위한 렌더러를 shlex 모듈에 추가할 수 있습니다. 이 렌더러의 이름은 sh이며, 템플릿 리터럴의 각 필드 값에 shlex.quote를 호출하는 것과 동등합니다.

따라서:

os.system(shlex.sh(t'cat {myfile}'))

위 코드는 다음과 동일하게 동작합니다:

os.system('cat ' + shlex.quote(myfile)))

구현은 다음과 같습니다:

def sh(template: TemplateLiteral):
    def render_field(value, format_spec, conversion_spec)
        field_text = format(value, format_spec, conversion_spec)
        return quote(field_text)
    return template.render(render_field=render_field)

shlex.sh가 추가되더라도 shell=True를 전달하는 것은 피하는 것이 좋다는 subprocess 문서의 기존 권고는 바뀌지 않으며, os.system() 문서에서 더 고수준의 subprocess API를 참조하도록 안내하는 부분도 바뀌지 않습니다.

subprocess 모듈 변경 사항

shlex 모듈에 렌더러가 추가되고 템플릿 리터럴이 도입되면, subprocess 모듈이 이미 시퀀스나 문자열을 받아 각각 다르게 동작하는 것처럼, 템플릿 리터럴도 Popen의 추가 입력 타입으로 받아 처리하도록 변경할 수 있습니다.

템플릿 리터럴이 도입되면, subprocess.Popen과 그에 따라 subprocess.run() 같은 모든 고수준 함수가 (적어도 POSIX 시스템에서는) 문자열을 안전한 방식으로 받을 수 있습니다.

예를 들어:

subprocess.run(t'cat {myfile}', shell=True)

위 코드는 이 PEP에서 제공하는 shlex.sh 렌더러를 자동으로 사용합니다. 따라서 다음과 같이 subprocess.run 호출 안에서 shlex를 사용하는 것은:

subprocess.run(shlex.sh(t'cat {myfile}'), shell=True)

run이 모든 템플릿 리터럴을 shlex.sh를 통해 자동으로 렌더링하므로 불필요한 중복이 됩니다.

또는 subprocess.Popen을 shell=True 없이 실행할 때에도 subprocess에 더 편리한 문법을 제공할 수 있습니다. 예를 들어:

subprocess.run(t'cat {myfile} --flag {value}')

위 코드는 다음과 동등합니다:

subprocess.run(['cat', myfile, '--flag', value])

또는 더 정확하게는:

subprocess.run(shlex.split(f'cat {shlex.quote(myfile)} --flag {shlex.quote(value)}'))

이는 앞에서와 같이 먼저 shlex.sh 렌더러를 사용한 다음, 그 결과에 shlex.split을 적용하는 방식으로 이루어집니다.

subprocess.Popen._execute_child 내부의 구현은 다음과 같을 것입니다:

if hasattr(args, "raw_template"):
    import shlex
    if shell:
        args = [shlex.sh(args)]
    else:
        args = shlex.split(shlex.sh(args))

교육 방법

이 PEP는 교육 환경에서 항상 사용할 수 있는 두 가지 표준 렌더러를 의도적으로 포함합니다. format() 내장 함수와 새로운 shlex.sh POSIX 셸 렌더러입니다.

이 두 렌더러를 함께 사용하면, 학생들이 f-문자열로 문자열 포매팅을 처음 배운 내용을 바탕으로 지연 렌더링에 대한 초기 이해를 쌓을 수 있습니다. 이 초기 이해의 목표는 학생들이 기존의 템플릿 렌더링 함수와 함께 템플릿 리터럴을 효과적으로 사용할 수 있도록 하는 것입니다.

예를 들어, f"{'some text'}", f"{value}", f"{value!r}", , f"{callable()}"를 모두 소개할 수 있습니다.

그런 다음 같은 연산을 format(t"{'some text'}"), format(t"{value}"), format(t"{value!r}"), , format(t"{callable()}")로 다시 작성하여 즉시 렌더링 형식과 지연 렌더링 형식 사이의 관계를 보여 줄 수 있습니다.

이어서 템플릿 리터럴을 지역 변수에 저장하고 그 표현을 format 호출 결과와 따로 살펴봄으로써, “템플릿 정의 시점”(또는 “보간 시점”)과 “템플릿 렌더링 시점”의 차이를 더 깊이 탐구할 수 있습니다. 이 시점에서 t"{callable!()}" 문법을 소개하여, 템플릿 정의 시점에 호출되는 필드 표현식과 템플릿 렌더링 시점에 호출되는 필드 표현식을 구별할 수 있습니다.

마지막으로, f"{'some text'}", format(t"{'some text'}"), shlex.sh(t"{'some text'}")의 결과 간 차이를 살펴봄으로써 기본 렌더링 함수와 사용자 정의 렌더링 함수 사이에 차이가 생길 수 있음을 보여 줄 수 있습니다.

그다음으로, 사용자 정의 템플릿 렌더링 함수를 직접 정의하는 것은 별도의 더 고급 주제가 됩니다(학생들이 데코레이터와 컨텍스트 관리자를 직접 작성하는 법을 배우기 훨씬 전에 이를 잘 사용하는 법부터 배우는 것이 일반적인 것과 비슷합니다).

PEP 750에는 지연 렌더링 주제의 여러 측면을 가르치기 위한 추가 아이디어가 담겨 있습니다.

논의

이전 논의는 PEP 498을 참조하십시오. 그곳의 여러 논점이 이 PEP에도 적용됩니다. PEP 750의 설계 논의 역시 매우 밀접한 관련이 있습니다. 현재 설계의 여러 측면이 그 PEP에서 영감을 받았기 때문입니다.

바이너리 보간 지원

f-문자열이 바이트 문자열을 다루지 않으므로, t-문자열도 바이트 문자열을 다루지 않습니다.

str 전용 인터페이스와의 상호 운용성

문자열만 받는 인터페이스와 상호 운용하기 위해, 호출되는 함수에 렌더링을 위임하는 대신 여전히 format()으로 보간 템플릿을 미리 렌더링할 수 있습니다.

이는 기본 렌더링을 항상 즉시 적용하며 렌더러 선택을 코드의 다른 부분에 위임할 방법이 없는 PEP 498과의 핵심적인 차이를 보여 줍니다.

원시 템플릿 문자열 보존

이 PEP의 이전 버전에서는 템플릿 리터럴에서 원시 템플릿 문자열을 사용할 수 없었습니다. 이를 유지하면 더 보기 좋은 템플릿 표현을 제공할 수 있을 뿐만 아니라, 표현식 텍스트와 형식 지정자 안에서 즉시 렌더링된 치환 필드의 세부 사항을 모두 포함하여 원래 문자열을 정확하게 재구성할 수도 있습니다.

전역 이름 조회 대신 풍부한 객체 생성

이 PEP의 이전 버전에서는 보간 함수가 나중에 사용할 새로운 종류의 객체를 생성하는 대신 __interpolate__ 내장 함수를 사용했습니다. 유용한 기본 렌더러를 갖춘 풍부한 서술적 객체를 생성하자 보간 의미를 사용자 정의할 수 있도록 지원하기가 훨씬 쉬워졌습니다.

f-문자열을 대체하는 대신 그 위에 구축

이 PEP의 이전 버전은 PEP 498 (f-문자열)을 완전히 대체하려고 했습니다. 그 PEP가 수락되고 최근에 PEP 701까지 수락되었으므로, 이 PEP는 대신 기존 f-문자열의 즉시 렌더링 위에 더 유연한 지연 렌더링 기능을 구축할 수 있습니다.

f-문자열이 보조 기능으로 존재한다고 가정함으로써 이 PEP의 제안 중 여러 측면(예를 들어 형식 지정자 안의 치환 필드를 처리하는 방법)이 단순해졌습니다.

반복 및 연결 의미 정의

이 PEP는 TemplateLiteral과 TemplateLiteralText에 대한 반복 및 연결 의미를 명시적으로 정의합니다. 엄밀히 필요한 것은 아니지만, 이를 정의해 두면 그동안 일반 문자열만 지원해 온 코드에서 이 타입들을 더 쉽게 다룰 수 있을 것으로 예상됩니다.

지연 필드 평가를 위한 새로운 변환 지정자

처음 공개된 PEP 750 버전에서는 모든 보간 필드에 기본적으로 지연 평가를 적용했습니다. 이후 (f-문자열과 이 PEP에서처럼) 즉시 평가를 기본값으로 하도록 변경되었지만, 이 주제에 관한 논의에서 보간된 필드 값을 수정 없이 사용하는 대신 렌더링 시점에 호출해야 한다는 것을 렌더링 함수에 알리는 방법을 제공하자는 아이디어가 나왔습니다.

PEP 750은 변환 지정자 처리도 평가 시점까지 미루었으므로, 인자 없이 __call__을 호출하는 것을 __repr__ (!a, !r) 또는 __str__ (!s)를 호출하는 기존 변환 지정자와 유사한 것으로 볼 수 있다는 제안이 나왔습니다.

이에 따라 이 PEP는 변환 지정자 처리 역시 렌더링 함수의 책임으로 하고, 지연 평가를 위한 새로운 변환 지정자로 !() 문법을 도입하도록 갱신되었습니다.

그러고 나면 operator.convert_field()를 추가하고 format() 내장 함수를 갱신하는 일은, 기본 변환 지정자를 받아들이려는 렌더링 함수 구현을 적절히 지원하는 문제일 뿐이었습니다.

사용자 정의 렌더러에서 임의의 변환 지정자 허용

새로운 변환 지정자로 !() 문법을 받아들이려면 파서가 변환 지정자로 받아들이는 문법을 반드시 갱신해야 합니다(현재 변환 지정자는 식별자로 제한되어 있습니다). 그러자 f-문자열 컴파일이 부과하는 추가 제한, 즉 변환 지정자가 정확히 !a, !r, !s 중 하나여야 한다는 제한을 t-문자열 컴파일에도 강제해야 하는지에 대한 질문이 제기되었습니다.

t-문자열은 컴파일될 때 !()를 허용하도록 이미 갱신되고 있었습니다. 그래서 형식 지정자가 개별 객체의 포매팅과 관련되듯이, 변환 지정자는 렌더링 함수와 관련된 것으로 취급하는 것이 타당했습니다. 파싱상의 이유로 제외되는 일부 문자를 빼면, 변환 지정자는 자유 텍스트 필드이며 그 의미는 이를 소비하는 함수나 객체가 결정합니다. 이렇게 하면 템플릿의 형식 지정자에 렌더러별 메타포매팅을 도입하려는 유혹이 줄어듭니다(렌더러별 정보는 모두 변환 지정자에 대신 넣을 수 있기 때문입니다).

새로운 문자열 접두사를 하나만 예약하기

이 PEP와 PEP 750의 주된 차이점은 후자가 임의의 문자열 접두사를 사용할 수 있게 하는 것을 목표로 한다는 점입니다. 이 PEP처럼 템플릿 리터럴 인스턴스를 생성해 다른 API에 전달하도록 요구하지 않습니다. 예를 들어 PEP 750에서는 이 PEP에서 설명하는 sh 렌더러를 sh"cat {somefile}"처럼 사용할 수 있습니다. 템플릿 리터럴을 명시적으로 생성해 일반 함수 호출에 전달할 필요가 없습니다(sh(t"cat {somefile}")에서처럼).

PEP 작성자들이 두 번째 표기를 선호하는 주된 이유는 무슨 일이 일어나는지를 독자에게 더 명확하게 보여 주기 때문입니다. 즉, 템플릿 리터럴 인스턴스가 생성된 다음, 보간 템플릿 인스턴스로 유용한 일을 할 줄 아는 호출 가능 객체에 전달됩니다.

PEP 750 저자 중 한 명의 초안 제안 역시 정적 타입 검사기가 특정 도메인 특화 언어의 사용을 추론할 수 있다고 봅니다. 직접 태그된 문자열에서만큼이나 명시적 함수 호출을 사용하는 형태에서도 쉽게 추론할 수 있다는 것입니다.

태그된 문자열 문법은 구문의 전체적인 표현력을 높이지 못하면서 사람 독자에게는 명확성을 떨어뜨린다고 볼 여지가 적어도 있습니다. 따라서 실행 가능한 가장 작은 제안(새로운 문자열 접두사 하나)으로 시작하는 것이 합리적으로 보입니다. 임의의 접두사로 일반화하는 것의 잠재적 가치는 나중에 다시 검토하면 됩니다.

덜 중요하지만 여전히 실질적인 고려 사항도 있습니다. 이 사용 사례에 새로운 문자열 접두사를 하나만 사용하면 앞으로 대체 접두사를 정의할 가능성이 열려 있습니다. 그런 접두사는 여전히 TemplateLiteral 객체를 생성하되, 문자열 내부에서 보간 필드를 정의할 때 다른 문법을 사용할 수 있습니다(아래의 i18n 논의를 참고하십시오).

더 간결한 지연 평가 문법에 대한 검토 연기

지연 평가에 대한 논의 중에 {-> expr}이 이미 지원되는 lambda 기반 문법인 {(lambda: expr)}의 잠재적인 문법적 설탕으로 제안되었습니다. 기존 문법에서는 : 문자가 형식 지정자의 시작으로 잘못 해석되지 않도록 괄호가 필요합니다.

이러한 표기를 추가하면 이 PEP에서 제안하는 렌더링 시점 함수 호출 문법(즉, 렌더링 시점에 임의의 표현식을 평가하기 위해 {-> expr!()}를 작성하는 것)을 보완하겠지만, PEP 작성자들은 이 주제를 이 PEP나 PEP 750이 수락될 경우 향후 PEP에 맡기는 것이 더 낫다고 생각합니다.

로깅 통합 가능성에 대한 검토 연기

logging 모듈의 과제 중 하나는 printf 스타일 포매팅에서 벗어나기 위한 합리적인 마이그레이션 전략을 지금까지 고안하지 못했다는 점입니다. logging 모듈에서는 포매터가 str.format() 또는 string.Template 스타일 치환을 사용하도록 지정할 수 있습니다. 하지만 그런 방식으로 작성된 메시지가 해당 문법을 기대하는 로그 레코드 포매터에서만 처리되도록 보장하기는 번거로울 수 있습니다.

로깅 메시지의 런타임 파싱과 보간에 드는 오버헤드도 모니터링 목적으로 런타임 이벤트를 광범위하게 로깅할 때 문제가 됩니다.

이 초기 PEP의 범위를 벗어나기는 하지만, logging 모듈의 이벤트 보고 API에 템플릿 리터럴 지원을 추가할 수도 있습니다. 그러면 다음과 같은 형태로 관련 세부 정보를 포착할 수 있습니다:

logging.debug(t"Event: {event}; Details: {data}")
logging.critical(t"Error: {error}; Details: {data}")

이는 기존의 mod 포매팅 스타일을 대신하는 것입니다:

logging.debug("Event: %s; Details: %s", event, data)
logging.critical("Error: %s; Details: %s", event, data)

템플릿 리터럴은 일반 인자로 전달되므로 다른 키워드 인자도 계속 사용할 수 있습니다:

logging.critical(t"Error: {error}; Details: {data}", exc_info=True)

이 PEP에서 설명하는 지연 필드 평가의 표준화 방식은 주로 logging 모듈과의 이 가상의 통합에서 예상되는 요구 사항을 바탕으로 합니다:

 logging.debug(t"Eager evaluation of {expensive_call()}")
 logging.debug(t"Lazy evaluation of {expensive_call!()}")

 logging.debug(t"Eager evaluation of {expensive_call_with_args(x, y, z)}")
 logging.debug(t"Lazy evaluation of {(lambda: expensive_call_with_args(x, y, z))!()}")

로깅 포매터의 정의가 템플릿 문자열을 지원하도록 갱신될지는 아직 열린 질문입니다. 만약 갱신된다면, 즉시 해석하지 않고 로그 레코드에서 조회해야 하는 필드를 정의하는 가장 유력한 방법은 단순합니다. 해당 필드를 이스케이프하여 리터럴 텍스트의 일부로 사용할 수 있게 하는 것입니다:

proc_id = get_process_id()
formatter = logging.Formatter(t"{{asctime}}:{proc_id}:{{name}}:{{levelname}}{{message}}")

i18n 사용 사례에서의 활용 가능성에 대한 검토 연기

이 PEP의 처음 동기가 된 사용 사례는 i18n(국제화) 번역을 위한 더 깔끔한 문법을 제공하는 것이었습니다. 번역에는 수정되지 않은 원래 템플릿에 대한 접근이 필요하기 때문입니다. 따라서 이 PEP는 Python의 string.Template 형식화와 Mozilla의 l20n 프로젝트에서 사용하는 치환 문법과의 호환성에 초점을 맞추었습니다.

그러나 이후의 논의를 통해 i18n 사용 사례에는 고려해야 할 중요한 사항이 더 있다는 사실이 드러났습니다. 이런 사항은 더 단순한 경우에는 영향을 주지 않습니다. 보안에 민감한 컨텍스트(HTML, 시스템 셸, 데이터베이스 쿼리 등)로의 보간을 처리하는 경우와, (최종 사용자의 모국어가 아니라) 개발 팀이 선호하는 언어로 애플리케이션 디버깅 메시지를 생성하는 경우가 그렇습니다.

이러한 깨달음에 따라 이 PEP는 str.format() 치환 문법을 사용하도록 바뀌었습니다. 이 문법은 원래 PEP 3101에서 정의되었고 이후 PEP 498의 기반으로 사용되었습니다.

이론적으로는 string.Template를 갱신하여 네이티브 템플릿 리터럴로부터 인스턴스를 생성하는 것을 지원하고 구조적 typing.Template 프로토콜을 구현하도록 할 수 있겠지만, PEP 작성자들은 그렇게 하는 데 따르는 실질적인 이점을 찾지 못했습니다.

그러나 이 PEP에서 사용하는 “단 하나의 문자열 접두사” 접근 방식의 중요한 이점 하나는, 기존 f-문자열 보간 문법을 일반화하여 t-문자열을 통한 지연 렌더링을 지원하면서도, 그것이 Python이 앞으로 제공할 유일한 컴파일러 지원 보간 문법이어야 한다는 의미를 내포하지는 않는다는 점입니다.

특히 이 방식은 대체 “t$-문자열” 문법의 가능성을 열어 둡니다. 이 문법을 쓰면 PEP 3101 기반 문법 대신 PEP 292 기반 보간 문법으로 TemplateLiteral 인스턴스를 생성할 수 있습니다:

template = t$”런타임에 템플릿의 $words와 ${other_values}를 치환합니다”

그런 방식으로 생성된 템플릿과 일반 t-문자열로 생성된 템플릿의 런타임상 유일한 차이는 raw_template 속성의 내용에 있을 것입니다.

비 POSIX 셸에 대한 이스케이프 렌더링 지원 연기

shlex.quote()는 정규식 문자 집합 [\w@%+=:,./-]를 안전한 것으로 분류하고 그 밖의 모든 문자는 안전하지 않은 것으로 간주합니다. 따라서 그런 문자가 포함된 문자열은 따옴표로 감싸야 합니다. 이때 사용되는 따옴표 처리 방식은 POSIX 셸에서 문자열 따옴표 처리가 동작하는 방식에 특화되어 있습니다. 따라서 POSIX 셸의 문자열 따옴표 규칙을 따르지 않는 셸을 실행할 때는 신뢰할 수 없습니다.

예를 들어 subprocess.run(f'echo {shlex.quote(sys.argv[1])}', shell=True)를 실행하는 것은 POSIX 따옴표 규칙을 따르는 셸을 사용할 때는 안전하지만:

$ cat > run_quoted.py
import sys, shlex, subprocess
subprocess.run(f"echo {shlex.quote(sys.argv[1])}", shell=True)
$ python3 run_quoted.py pwd
pwd
$ python3 run_quoted.py '; pwd'
; pwd
$ python3 run_quoted.py "'pwd'"
'pwd'

Python에서 셸을 실행할 때 cmd.exe(또는 Powershell)가 호출되는 경우에는 여전히 안전하지 않습니다:

S:\> echo import sys, shlex, subprocess > run_quoted.py
S:\> echo subprocess.run(f"echo {shlex.quote(sys.argv[1])}", shell=True) >> run_quoted.py
S:\> type run_quoted.py
import sys, shlex, subprocess
subprocess.run(f"echo {shlex.quote(sys.argv[1])}", shell=True)
S:\> python3 run_quoted.py "echo OK"
'echo OK'
S:\> python3 run_quoted.py "'& echo Oh no!"
''"'"'
Oh no!'

이러한 표준 라이브러리의 한계를 해결하는 것은 이 PEP의 범위를 벗어납니다.

감사의 글

  • PEP 498을 작성하고 문자열 보간에서 임의의 표현식 치환이 가능함을 보여 준 Eric V. Smith
  • 태그된 문자열이 이 PEP에 영감을 준 상당한 설계 개선, 언어 수준의 지연 템플릿 렌더링 지원이 지닌 가치에 대한 전반적인 옹호, 그리고 모든 네이티브 보간 템플릿 지원이 향후 도메인 특화 언어를 위한 견고한 문법 강조 및 정적 타입 검사 지원을 제공하려는 노력에 튼튼한 기반을 마련하도록 힘써 준 것에 대해 PEP 750의 작성자들에게 감사드립니다.
  • Barry Warsaw, Armin Ronacher, Mike Miller: 이 지연 렌더링 모델을 i18n 사용 사례에 적용할 수 있는지 탐구하는 데 기여해 주셔서 감사드립니다(최종 결론은 적어도 현재 Python에서 i18n을 다루는 방식에는 잘 맞지 않는다는 것이었습니다).

참고 문헌