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

Python 개선 제안 한국어 번역

PEP 750 – 템플릿 문자열

Author:
Jim Baker <jim.baker at python.org>, Guido van Rossum <guido at python.org>, Paul Everitt <pauleveritt at me.com>, Koudai Aono <koxudaxi at gmail.com>, Lysandros Nikolaou <lisandrosnik at gmail.com>, Dave Peck <davepeck at davepeck.org>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Created:
08-Jul-2024
Python-Version:
3.14
Post-History:
09-Aug-2024, 17-Oct-2024, 21-Oct-2024, 18-Nov-2024
Resolution:
10-Apr-2025

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 Template strings.

×

See PEP 1 for how to propose changes.

초록

이 PEP는 사용자 지정 문자열 처리를 위한 템플릿 문자열을 소개합니다.

템플릿 문자열은 f-문자열의 일반화로, f 접두사 대신 t를 사용합니다. t-문자열은 str로 평가되는 대신 새로운 타입인 Template로 평가됩니다:

template: Template = t"Hello {name}"

템플릿을 사용하면 개발자가 문자열과 문자열의 보간된 값에 결합되기 전에 접근할 수 있습니다. 이를 통해 Python 언어에 유연한 문자열 처리를 기본적으로 제공하고, 안전성 검사, 웹 템플릿, 도메인별 언어 등을 가능하게 합니다.

다른 PEP와의 관계

Python은 PEP 498을 통해 Python 3.6에서 f-문자열을 도입했습니다. 이후 문법은 PEP 701에서 정식으로 정의되었으며, 일부 제한도 해제되었습니다. 이 PEP는 PEP 701을 기반으로 합니다.

PEP 498이 발표된 시기와 거의 동시에, PEP 501은 “i-문자열”—즉, “보간 템플릿 문자열”—을 제공하기 위해 작성되었습니다. 이 PEP는 f-문자열에 대한 추가 경험을 기다리며 보류되었습니다. 2023년 3월에 다른 작성자가 이 PEP 작업을 재개하여, 템플릿 리터럴 문자열인 “t-문자열”을 도입하고 PEP 701을 기반으로 구축했습니다.

이 PEP의 작성자들은 이를 PEP 501의 갱신된 작업을 일반화하고 단순화한 것으로 봅니다. (해당 PEP도 이 PEP의 새로운 아이디어를 반영하도록 최근 갱신되었습니다.)

동기

Python f-문자열은 사용하기 쉽고 매우 널리 사용됩니다. 그러나 시간이 지나면서 개발자들은 이를 특정 사용 사례에 적합하지 않게 만드는 한계를 발견했습니다. 특히 f-문자열은 보간된 값이 최종 문자열로 결합되기 전에 이를 가로채 변환할 방법을 제공하지 않습니다.

그 결과 f-문자열을 부주의하게 사용하면 보안 취약점이 발생할 수 있습니다. 예를 들어, sqlite3을 사용하여 SQL 쿼리를 실행하는 사용자는 SQL 표현식에 값을 삽입하기 위해 f-문자열을 사용하고 싶을 수 있으며, 이는 SQL 삽입 공격으로 이어질 수 있습니다. 또는 HTML을 작성하는 개발자가 문자열에 이스케이프되지 않은 사용자 입력을 포함하면 사이트 간 스크립팅(XSS) 취약점이 발생할 수 있습니다.

더 넓게 보면, 보간된 값을 최종 문자열로 결합하기 전에 변환할 수 없다는 점은 더 복잡한 문자열 처리 작업에서 f-문자열의 유용성을 제한합니다.

템플릿 문자열은 개발자에게 문자열과 문자열의 보간된 값에 접근할 수 있도록 하여 이러한 문제를 해결합니다.

예를 들어 HTML을 생성하려 한다고 가정해 보겠습니다. 템플릿 문자열을 사용하면 콘텐츠를 자동으로 정제할 수 있는 html() 함수를 정의할 수 있습니다:

evil = "<script>alert('evil')</script>"
template = t"<p>{evil}</p>"
assert html(template) == "<p>&lt;script&gt;alert('evil')&lt;/script&gt;</p>"

마찬가지로 가상의 html() 함수는 개발자가 딕셔너리를 사용하여 HTML 요소에 속성을 쉽게 추가할 수 있도록 합니다:

attributes = {"src": "shrubbery.jpg", "alt": "looks nice"}
template = t"<img {attributes} />"
assert html(template) == '<img src="shrubbery.jpg" alt="looks nice" />'

이 예시들은 모두 f-문자열로는 구현할 수 없습니다. 템플릿 문자열은 보간된 값을 가로채 변환하는 메커니즘을 제공함으로써 광범위한 문자열 처리 사용 사례를 가능하게 합니다.

사양

템플릿 문자열 리터럴

이 PEP는 템플릿 문자열 리터럴을 정의하기 위한 새로운 문자열 접두사 t를 도입합니다. 이러한 리터럴은 표준 라이브러리 모듈 string.templatelib에 있는 새로운 타입 Template으로 평가됩니다.

다음 코드는 Template 인스턴스를 생성합니다.

from string.templatelib import Template
template = t"This is a template string."
assert isinstance(template, Template)

템플릿 문자열 리터럴은 PEP 701의 전체 구문을 지원합니다. 여기에는 보간 내부에 템플릿 문자열을 중첩하는 기능과 유효한 모든 따옴표(', ", ''', """)를 사용하는 기능이 포함됩니다. 다른 문자열 접두사와 마찬가지로 t 접두사는 따옴표 바로 앞에 와야 합니다. f-문자열과 마찬가지로 소문자 t와 대문자 T 접두사를 모두 지원합니다. f-문자열과 마찬가지로 t-문자열은 u 또는 b 접두사와 결합할 수 없습니다.

또한 f-문자열과 t-문자열은 결합할 수 없으므로 ft 접두사는 유효하지 않습니다. t-문자열은 may r 접두사와 결합할 수 있습니다. 자세한 내용은 아래의 Raw Template Strings 절을 참조하십시오.

Template 타입

템플릿 문자열은 새로운 불변 타입인 string.templatelib.Template의 인스턴스로 평가됩니다.

class Template:
    strings: tuple[str, ...]
    """
    A non-empty tuple of the string parts of the template,
    with N+1 items, where N is the number of interpolations
    in the template.
    """

    interpolations: tuple[Interpolation, ...]
    """
    A tuple of the interpolation parts of the template.
    This will be an empty tuple if there are no interpolations.
    """

    def __new__(cls, *args: str | Interpolation):
        """
        Create a new Template instance.

        Arguments can be provided in any order.
        """
        ...

    @property
    def values(self) -> tuple[object, ...]:
        """
        Return a tuple of the `value` attributes of each Interpolation
        in the template.
        This will be an empty tuple if there are no interpolations.
        """
        ...

    def __iter__(self) -> Iterator[str | Interpolation]:
        """
        Iterate over the string parts and interpolations in the template.

        These may appear in any order. Empty strings will not be included.
        """
        ...

stringsinterpolations 특성을 통해 리터럴의 문자열 부분과 모든 보간에 액세스할 수 있습니다.

name = "World"
template = t"Hello {name}"
assert template.strings[0] == "Hello "
assert template.interpolations[0].value == "World"

Interpolation 타입

Interpolation 타입은 템플릿 문자열 내부의 표현식을 나타냅니다. Template과 마찬가지로 string.templatelib 모듈에 있는 새로운 클래스입니다.

class Interpolation:
    value: object
    expression: str
    conversion: Literal["a", "r", "s"] | None
    format_spec: str

    __match_args__ = ("value", "expression", "conversion", "format_spec")

    def __new__(
        cls,
        value: object,
        expression: str = "",
        conversion: Literal["a", "r", "s"] | None = None,
        format_spec: str = "",
    ):
        ...

Interpolation 타입은 얕은 불변성을 가집니다. 해당 특성은 재할당할 수 없습니다.

value 특성은 보간을 평가한 결과입니다.

name = "World"
template = t"Hello {name}"
assert template.interpolations[0].value == "World"

템플릿 문자열 리터럴에서 보간을 생성하면 expression 특성에 보간의 original text가 포함됩니다.

name = "World"
template = t"Hello {name}"
assert template.interpolations[0].expression == "name"

개발자가 Interpolation을 명시적으로 생성하는 경우 expression 특성에 값을 선택적으로 제공할 수 있습니다. 문자열로 저장되더라도, 이는 유효한 Python 표현식이어야 합니다. 값을 제공하지 않으면 expression 특성은 빈 문자열("")로 기본 설정됩니다.

대부분의 템플릿 처리 코드에서는 expression 특성을 사용하지 않을 것으로 예상합니다. 이 특성은 완전성을 확보하고 디버깅 및 인트로스펙션에 사용하기 위해 제공됩니다. 템플릿 문자열을 처리하는 방법에 대한 자세한 내용은 Common Patterns Seen in Processing Templates 절과 Examples 절을 모두 참조하십시오.

conversion 특성은 사용할 optional conversion이며, repr(), str(), ascii() 변환에 각각 대응하는 r, s, a 중 하나입니다. f-문자열과 마찬가지로 다른 변환은 지원되지 않습니다.

name = "World"
template = t"Hello {name!r}"
assert template.interpolations[0].conversion == "r"

변환을 제공하지 않으면 conversionNone입니다.

format_spec 특성은 format specification입니다. f-문자열과 마찬가지로 이는 값을 표시하는 방법을 정의하는 임의의 문자열입니다.

value = 42
template = t"Value: {value:.2f}"
assert template.interpolations[0].format_spec == ".2f"

f-문자열의 형식 지정에는 그 자체로 보간이 포함될 수 있습니다. 템플릿 문자열에서도 이를 허용하며, format_spec은 즉시 평가된 결과로 설정됩니다.

value = 42
precision = 2
template = t"Value: {value:.{precision}f}"
assert template.interpolations[0].format_spec == ".2f"

형식 지정이 제공되지 않으면, format_spec은 빈 문자열("")로 기본 설정됩니다. 이는 Python의 format() 내장 함수의 format_spec매개변수와 일치합니다.

f-문자열과 달리 템플릿을 처리하는 코드가 conversionformat_spec속성을 어떻게 해석할지 결정합니다. 이러한 코드는 이 속성을 반드시 사용할 필요는 없지만, 속성이 있으면 이를 존중해야 하며 가능한 한 f-문자열의 동작과 일치해야 합니다. 예를 들어 {value:.2f}를 사용하는 템플릿 문자열을 처리할 때 값이 소수점 이하 두 자리로 반올림되지 않는다면 놀라운 일일 것입니다.

Template.values 속성

Template.values속성은 템플릿의 각 Interpolation에서 value특성에 액세스하기 위한 단축 표현이며 다음과 같습니다.

@property
def values(self) -> tuple[object, ...]:
    return tuple(i.value for i in self.interpolations)

Template 내용 순회

Template.__iter__()메서드는 템플릿의 전체 내용에 간단히 액세스하는 방법을 제공합니다. 이 메서드는 문자열 부분과 보간을 나타나는 순서대로 반환하며, 빈 문자열은 생략합니다.

__iter__()메서드는 다음과 같습니다.

def __iter__(self) -> Iterator[str | Interpolation]:
    for s, i in zip_longest(self.strings, self.interpolations):
        if s:
            yield s
        if i:
            yield i

다음 예제에서는 __iter__()메서드의 작동을 보여 줍니다.

assert list(t"") == []

assert list(t"Hello") == ["Hello"]

name = "World"
template = t"Hello {name}!"
contents = list(template)
assert len(contents) == 3
assert contents[0] == "Hello "
assert contents[1].value == "World"
assert contents[1].expression == "name"
assert contents[2] == "!"

Template.strings에 있을 수 있는 빈 문자열은 __iter__()메서드의 출력에 포함되지 않습니다.

first = "Eat"
second = "Red Leicester"
template = t"{first}{second}"
contents = list(template)
assert len(contents) == 2
assert contents[0].value == "Eat"
assert contents[0].expression == "first"
assert contents[1].value == "Red Leicester"
assert contents[1].expression == "second"

# However, the strings attribute contains empty strings:
assert template.strings == ("", "", "")

템플릿 처리 코드는 요구 사항과 편의성에 따라 strings, interpolations, values__iter__()를 원하는 조합으로 사용할 수 있습니다.

템플릿 문자열 처리

개발자는 템플릿 문자열을 처리하는 임의의 코드를 작성할 수 있습니다. 예를 들어 다음 함수는 템플릿의 정적 부분을 소문자로, 보간을 대문자로 렌더링합니다.

from string.templatelib import Template, Interpolation

def lower_upper(template: Template) -> str:
    """Render static parts lowercased and interpolations uppercased."""
    parts: list[str] = []
    for item in template:
        if isinstance(item, Interpolation):
            parts.append(str(item.value).upper())
        else:
            parts.append(item.lower())
    return "".join(parts)

name = "world"
assert lower_upper(t"HELLO {name}") == "hello WORLD"

템플릿 문자열을 특정한 방식으로 처리해야 한다는 요구 사항은 없습니다. 템플릿을 처리하는 코드는 문자열을 반환할 의무가 없습니다. 템플릿 문자열은 유연한 범용 기능입니다.

템플릿 문자열을 처리하는 방법에 대한 자세한 내용은 Common Patterns Seen in Processing Templates 섹션을 참조하십시오. 자세한 작업 예제는 Examples 섹션을 참조하십시오.

템플릿 문자열 연결

템플릿 문자열은 +를 사용한 명시적 연결을 지원합니다. 두 Template인스턴스 간 연결은 Template.__add__()를 통해 지원됩니다.

name = "World"

assert isinstance(t"Hello " + t"{name}", Template)
assert (t"Hello " + t"{name}").strings == ("Hello ", "")
assert (t"Hello " + t"{name}").values[0] == "World"

두 템플릿 문자열 리터럴의 암시적 연결도 지원됩니다.

name = "World"
assert isinstance(t"Hello " t"{name}", Template)
assert (t"Hello " t"{name}").strings == ("Hello ", "")
assert (t"Hello " t"{name}").values[0] == "World"

Templatestr의 암시적 연결과 명시적 연결은 모두 금지됩니다. 이는 str을 정적 문자열 부분으로 처리해야 하는지 아니면 보간으로 처리해야 하는지가 모호하기 때문입니다.

Templatestr을 결합하려면 개발자가 str을 어떻게 처리할지 명시적으로 결정해야 합니다. str을 정적 문자열 부분으로 사용하려는 경우 Template로 감싸야 합니다. str을 보간 값으로 사용하려는 경우 Interpolation으로 감싼 후 Template생성자에 전달해야 합니다. 예를 들면 다음과 같습니다.

name = "World"

# Treat `name` as a static string part
template = t"Hello " + Template(name)

# Treat `name` as an interpolation
template = t"Hello " + Template(Interpolation(name, "name"))

템플릿과 보간의 동등성

TemplateInterpolation 인스턴스는 객체 동일성(is)으로 비교됩니다.

Template 인스턴스는 문자열이나 기타 모든 타입을 반환할 수 있는 템플릿 처리 코드에서 사용하도록 설계되었습니다. 이러한 타입은 필요에 따라 자체 동등성 의미를 제공할 수 있습니다.

순서 비교 지원 없음

TemplateInterpolation 타입은 순서 비교를 지원하지 않습니다. 이는 사전식 순서 비교를 지원하는 Python의 다른 모든 문자열 리터럴 타입과 다릅니다. 보간에는 임의의 값이 포함될 수 있으므로 보간에 자연스러운 순서는 없습니다. 따라서 Template이나 Interpolation 타입 어느 쪽도 표준 비교 메서드를 구현하지 않습니다.

디버그 지정자(=) 지원

디버그 지정자인 =는 템플릿 문자열에서 지원되며 f-문자열에서와 유사하게 동작하지만, 구현상의 제한으로 인해 약간의 차이가 있습니다.

특히 t'{value=}'t'value={value!r}'로 처리됩니다. 첫 번째 정적 문자열은 ""에서 "value="로 다시 작성되며 conversion의 기본값은 r입니다:

name = "World"
template = t"Hello {name=}"
assert template.strings[0] == "Hello name="
assert template.interpolations[0].value == "World"
assert template.interpolations[0].conversion == "r"

변환이 명시적으로 제공되면 그대로 유지됩니다: t'{value=!s}'t'value={value!s}'로 처리됩니다.

변환 없이 형식 문자열이 제공되면 conversionNone으로 설정됩니다: t'{value=:fmt}'t'value={value:fmt}'로 처리됩니다.

디버그 지정자에서는 공백이 보존되므로 t'{value = }'t'value = {value!r}'로 처리됩니다.

원시 템플릿 문자열

원시 템플릿 문자열은 rt(또는 tr) 접두사를 사용하여 지원됩니다:

trade = 'shrubberies'
template = rt'Did you say "{trade}"?\n'
assert template.strings[0] == r'Did you say "'
assert template.strings[1] == r'"?\n'

이 예제에서 \n은 줄 바꿈 문자가 아니라 두 개의 পৃথ어진 문자(백슬래시와 ‘n’)로 처리됩니다. 이는 Python의 원시 문자열 동작과 일관됩니다.

일반 템플릿 문자열과 마찬가지로 원시 템플릿 문자열의 보간도 정상적으로 처리되므로 원시 문자열 동작과 동적 콘텐츠를 결합할 수 있습니다.

보간 표현식 평가

보간의 표현식 평가는 PEP 498과 동일합니다:

문자열에서 추출된 표현식은 템플릿 문자열이 나타난 문맥에서 평가됩니다. 이는 표현식이 지역 변수와 전역 변수를 포함하여 해당 어휘적 스코프에 완전히 접근할 수 있음을 의미합니다. 함수 및 메서드 호출을 포함하여 유효한 모든 Python 표현식을 사용할 수 있습니다.

템플릿 문자열은 f-문자열과 마찬가지로 왼쪽에서 오른쪽으로 즉시 평가됩니다. 즉, 보간은 템플릿 문자열이 처리될 때 즉시 평가되며, 지연되거나 람다로 감싸지지 않습니다.

예외

t-문자열 리터럴에서 발생하는 예외는 f-문자열 리터럴에서 발생하는 예외와 동일합니다.

Template.__str__() 구현 없음

Template 타입은 특수한 __str__() 구현을 제공하지 않습니다.

이는 Template 인스턴스가 문자열이나 다른 모든 타입을 반환할 수 있는 템플릿 처리 코드에서 사용하도록 의도되었기 때문입니다. Template을 문자열로 변환하는 표준적인 방법은 없습니다.

TemplateInterpolation 타입은 모두 유용한 __repr__() 구현을 제공합니다.

다음은 string.templatelib 모듈입니다.

string 모듈은 새로운 templatelib 서브모듈을 포함하는 패키지로 변환되며, 이 서브모듈에는 TemplateInterpolation 형식이 포함됩니다. 이 PEP를 구현한 후에는 이 새로운 모듈을 convert()와 같은 관련 함수나 셸 스크립트 도우미와 같은 향후 템플릿 처리 코드에 사용할 수 있습니다.

예제

이 PEP의 이 섹션에 있는 모든 예제에는 공개 pep750-examples Git 저장소에서 이용할 수 있는 완전히 테스트된 참조 구현이 있습니다.

예제: t-문자열을 사용한 f-문자열 구현

t-문자열을 사용하면 f-문자열을 쉽게 “구현”할 수 있습니다. 즉, f-문자열 리터럴과 거의 같은 방식으로 Template을 처리하여 동일한 결과를 반환하는 f(template: Template) -> str함수를 작성할 수 있습니다.

name = "World"
value = 42
templated = t"Hello {name!r}, value: {value:.2f}"
formatted = f"Hello {name!r}, value: {value:.2f}"
assert f(templated) == formatted

f() 함수는 !r와 같은 변환 지정자와 :.2f와 같은 형식 지정자를 모두 지원합니다. 전체 코드는 상당히 간단합니다:

from string.templatelib import Template, Interpolation

def convert(value: object, conversion: Literal["a", "r", "s"] | None) -> object:
    if conversion == "a":
        return ascii(value)
    elif conversion == "r":
        return repr(value)
    elif conversion == "s":
        return str(value)
    return value

def f(template: Template) -> str:
    parts = []
    for item in template:
        match item:
            case str() as s:
                parts.append(s)
            case Interpolation(value, _, conversion, format_spec):
                value = convert(value, conversion)
                value = format(value, format_spec)
                parts.append(value)
    return "".join(parts)

Note

예제 코드

fstring.pytest_fstring.py를 참조하십시오.

예제: 구조화된 로깅

구조화된 로깅을 사용하면 개발자가 JSON과 같은 기계 판독 가능 형식으로 데이터를 기록할 수 있습니다. t-문자열을 사용하면 개발자는 단일 로그 문만으로 사람이 읽을 수 있는 메시지와 함께 구조화된 데이터를 쉽게 기록할 수 있습니다.

템플릿 문자열을 사용하여 구조화된 로깅을 구현하는 두 가지 접근법을 제시합니다.

접근법 1: 사용자 지정 로그 메시지

해당 Python Logging Cookbook에는 구조화된 로깅을 구현하는 방법에 관한 짧은 절이 있습니다.

로깅 Cookbook에서는 간단한 텍스트 메시지와 별도의 값 딕셔너리로 생성되는 새로운 “message” 클래스인 StructuredMessage를 만들 것을 제안합니다.

message = StructuredMessage("user action", {
    "action": "traded",
    "amount": 42,
    "item": "shrubs"
})
logging.info(message)

# Outputs:
# user action >>> {"action": "traded", "amount": 42, "item": "shrubs"}

StructuredMessage.__str__() 메서드는 사람이 읽을 수 있는 메시지 값을 모두 형식화하여 하나의 최종 문자열로 결합합니다. (전체 예제는 logging cookbook를 참조하십시오.)

템플릿 문자열을 사용하여 StructuredMessage의 개선된 버전을 구현할 수 있습니다.

import json
from string.templatelib import Interpolation, Template
from typing import Mapping

class TemplateMessage:
    def __init__(self, template: Template) -> None:
        self.template = template

    @property
    def message(self) -> str:
        # Use the f() function from the previous example
        return f(self.template)

    @property
    def values(self) -> Mapping[str, object]:
        return {
            item.expression: item.value
            for item in self.template
            if isinstance(item, Interpolation)
        }

    def __str__(self) -> str:
        return f"{self.message} >>> {json.dumps(self.values)}"

_ = TemplateMessage  # optional, to improve readability
action, amount, item = "traded", 42, "shrubs"
logging.info(_(t"User {action}: {amount:.2f} {item}"))

# Outputs:
# User traded: 42.00 shrubs >>> {"action": "traded", "amount": 42, "item": "shrubs"}

템플릿 문자열을 사용하면 사용자 지정 메시지 클래스를 정의하는 더욱 우아한 방법을 얻을 수 있습니다. 템플릿 문자열을 사용하면 개발자가 형식 문자열과 값 딕셔너리가 서로 일치하도록 유지할 필요가 더 이상 없으며, 단일 템플릿 문자열 리터럴 하나만 있으면 됩니다. TemplateMessage 구현은 각각 Interpolation.expressionInterpolation.value 속성에서 구조화된 키와 값을 자동으로 추출할 수 있습니다.

접근법 2: 사용자 지정 포매터

사용자 지정 메시지는 구조화된 로깅에 합리적인 접근법이지만 다소 번거로울 수 있습니다. 이를 사용하려면 개발자는 작성하는 모든 로그 메시지를 사용자 지정 클래스로 감싸야 합니다. 이를 잊기 쉽습니다.

대안으로 사용자 정의 logging.Formatter 클래스를 정의할 수 있습니다. 이 접근 방식은 더 유연하며 최종 출력을 더 세밀하게 제어할 수 있습니다. 특히 단일 템플릿 문자열을 가져와 여러 형식(사람이 읽을 수 있는 형식 및 JSON)으로 출력하여 로그 스트림을 분리할 수 있습니다.

사람이 읽을 수 있는 출력을 위한 MessageFormatter와 JSON 출력을 위한 ValuesFormatter라는 두 가지 간단한 포매터를 정의합니다:

import json
from logging import Formatter, LogRecord
from string.templatelib import Interpolation, Template
from typing import Any, Mapping


class MessageFormatter(Formatter):
    def message(self, template: Template) -> str:
        # Use the f() function from the previous example
        return f(template)

    def format(self, record: LogRecord) -> str:
        msg = record.msg
        if not isinstance(msg, Template):
            return super().format(record)
        return self.message(msg)


class ValuesFormatter(Formatter):
    def values(self, template: Template) -> Mapping[str, Any]:
        return {
            item.expression: item.value
            for item in template
            if isinstance(item, Interpolation)
        }

    def format(self, record: LogRecord) -> str:
        msg = record.msg
        if not isinstance(msg, Template):
            return super().format(record)
        return json.dumps(self.values(msg))

그런 다음 로거를 구성할 때 이러한 포매터를 사용할 수 있습니다:

import logging
import sys

logger = logging.getLogger(__name__)
message_handler = logging.StreamHandler(sys.stdout)
message_handler.setFormatter(MessageFormatter())
logger.addHandler(message_handler)

values_handler = logging.StreamHandler(sys.stderr)
values_handler.setFormatter(ValuesFormatter())
logger.addHandler(values_handler)

action, amount, item = "traded", 42, "shrubs"
logger.info(t"User {action}: {amount:.2f} {item}")

# Outputs to sys.stdout:
# User traded: 42.00 shrubs

# At the same time, outputs to sys.stderr:
# {"action": "traded", "amount": 42, "item": "shrubs"}

이 접근 방식은 구조화된 로깅에 사용자 정의 메시지 방식을 사용하는 것보다 몇 가지 장점이 있습니다:

  • 개발자는 t-문자열을 사용자 정의 클래스에 감싸지 않고 직접 로깅할 수 있습니다.
  • 사람이 읽을 수 있는 출력과 구조화된 출력을 별도의 로그 스트림으로 보낼 수 있습니다. 이는 사람이 읽을 수 있는 데이터와 독립적으로 구조화된 데이터를 처리하는 로그 집계 시스템에 유용합니다.

Note

예제 코드

logging.pytest_logging.py를 참조하십시오.

예제: HTML 템플릿화

이 PEP에는 몇 가지 짧은 HTML 템플릿화 예제가 포함되어 있습니다. Motivation 섹션(및 이 PEP의 몇몇 다른 부분)에서 언급한 “가설적인” html() 함수가 실제로 존재하며 pep750-examples 저장소에 제공된다는 사실이 밝혀졌습니다. 템플릿 문자열로 복잡한 문법을 구문 분석하는 것을 고려하고 있다면 유용하게 사용하시기를 바랍니다.

하위 호환성

f-문자열과 마찬가지로 템플릿 문자열의 사용은 이전 버전과 구문상 하위 호환되지 않습니다.

보안 영향

보간과 관련하여 템플릿 문자열을 사용할 때의 보안 영향은 다음과 같습니다:

  1. 스코프 조회는 f-문자열과 동일합니다(어휘적 스코프). 이 모델은 실제 사용에서 잘 작동하는 것으로 입증되었습니다.
  2. Template 인스턴스를 처리하는 코드는 보간이 나타나는 컨텍스트를 존중하는 것을 포함하여 모든 보간이 안전한 방식으로 처리되도록 보장할 수 있습니다.

이것을 가르치는 방법

템플릿 문자열에는 여러 대상 독자가 있습니다:

  • 템플릿 문자열 및 처리 함수를 사용하는 개발자
  • 템플릿 처리 코드 작성자
  • 템플릿 문자열로 흥미로운 기능을 구축하는 프레임워크 작성자

개발자에게 이것을 가르치는 일은 간단할 것으로 기대합니다. 언뜻 보면 템플릿 문자열은 f-문자열과 똑같아 보입니다. 구문은 익숙하며 스코핑 규칙도 동일하게 유지됩니다.

개발자가 가장 먼저 배워야 할 점은 템플릿 문자열 리터럴이 문자열로 평가되지 않고, 대신 새로운 형식인 Template로 평가된다는 것입니다. 이는 템플릿 처리 코드에서 사용하도록 의도된 단순한 형식입니다. 개발자가 처리 함수를 호출해야 비로소 원하는 결과를 얻습니다. 일반적으로는 문자열이지만, 처리 코드는 물론 임의의 형식을 반환할 수도 있습니다.

개발자는 템플릿 문자열이 f-문자열이나 str.format() 같은 다른 문자열 포매팅 메서드와 어떤 관계가 있는지도 이해해야 합니다. 각 메서드를 언제 사용할지 결정해야 합니다. 단순한 문자열만 필요하고 보안상의 영향이 없다면 f-문자열이 가장 좋은 선택일 가능성이 높습니다. 포맷 문자열을 사용하는 대부분의 경우에는 템플릿 문자열 생성을 감싸는 함수로 대체할 수 있습니다. 포맷 문자열을 사용자 입력, 파일 시스템 또는 데이터베이스에서 가져오는 경우에는 원한다면 이를 Template인스턴스로 변환하는 코드를 작성할 수 있습니다.

개발자는 t-문자열이 거의 항상 처리 함수와 함께 사용된다는 점을 배우게 되므로, 반드시 Template형식의 세부 사항까지 이해할 필요는 없습니다. 디스크립터와 데코레이터의 경우와 마찬가지로, t-문자열 처리 함수를 작성하는 개발자보다 이를 사용하는 개발자가 훨씬 많을 것으로 예상합니다.

시간이 지나면 더 숙련된 개발자 중 일부는 자신만의 템플릿 처리 코드를 작성하기를 원할 것입니다. 처리 코드를 작성하려면 형식 문법의 관점에서 사고해야 하는 경우가 많습니다. 개발자는 Template인스턴스의 stringsinterpolation 특성을 다루는 방법과 보간을 문맥에 민감한 방식으로 처리하는 방법을 배워야 합니다. 더 정교한 문법에는 추상 구문 트리(AST)와 같은 중간 표현으로의 구문 분석이 필요할 가능성이 높습니다. 우수한 템플릿 처리 코드는 적절한 경우 포맷 지정자와 변환을 처리합니다. 실제 운영 환경 수준의 템플릿 처리 코드를 작성하는 일은, 예를 들어 HTML 템플릿을 지원하는 경우처럼, 큰 작업이 될 수 있습니다.

템플릿 문자열은 프레임워크 작성자에게 도구 상자의 강력한 새 도구를 제공할 것으로 예상합니다. 템플릿 문자열의 기능은 템플릿 엔진과 같은 기존 도구와 겹치지만, t-문자열은 그 로직을 언어 자체로 옮깁니다. Python의 강력함과 일반성을 문자열 처리 작업에 온전히 활용하면 프레임워크 작성자에게 새로운 가능성이 열립니다.

왜 또 다른 템플릿 접근 방식입니까?

Python 생태계에는 이미 Jinja와 같이 널리 채택된 성숙한 템플릿 언어가 있습니다. 새로운 템플릿 시스템을 만들기 위한 지원을 구축하는 이유는 무엇입니까?

Jinja와 같은 프로젝트는 템플릿이 개발자에게 소프트웨어의 일부라기보다는 디자이너의 사용자 지정 작업이나 사용자가 만든 콘텐츠의 일부인 경우에도 여전히 필요합니다. 예를 들어 CMS가 이에 해당합니다.

프론트엔드 개발의 추세는 템플릿을 소프트웨어의 일부로 간주하고 개발자가 작성하는 방향으로 이어져 왔습니다. 개발자는 현대적인 언어 기능과 우수한 도구 사용 경험을 원합니다. PEP 750은 정적이지 않은 부분이 Python인 DSL을 구상합니다. 즉, 동일한 스코프 규칙, 타입 지정, 표현식 구문 등을 사용하는 DSL입니다.

템플릿 처리에서 볼 수 있는 일반적인 패턴

구조적 패턴 매칭

구조적 패턴 매칭을 사용하여 Template을 순회하는 것은 많은 템플릿 함수 구현에서 기대되는 모범 사례입니다:

from string.templatelib import Template, Interpolation

def process(template: Template) -> Any:
    for item in template:
        match item:
            case str() as s:
                ... # handle each string part
            case Interpolation() as interpolation:
                ... # handle each interpolation

처리 코드는 Interpolation형식의 속성에 대해 하위 패턴 매칭을 수행하는 경우도 많습니다:

match arg:
    case Interpolation(int()):
        ... # handle interpolations with integer values
    case Interpolation(value=str() as s):
        ... # handle interpolations with string values
    # etc.

메모이제이션

템플릿 함수는 템플릿의 정적 부분과 동적 부분을 모두 효율적으로 처리할 수 있습니다. Template 객체의 구조는 효과적인 메모이제이션을 가능하게 합니다.

strings = template.strings  # Static string parts
values = template.values  # Dynamic interpolated values

이러한 분리를 통해 처리된 정적 부분을 캐시하는 동시에 필요에 따라 동적 부분을 삽입할 수 있습니다. 템플릿 처리 코드를 작성하는 사람은 정적 strings를 캐시 키로 사용할 수 있으며, 유사한 템플릿이 반복해서 사용될 때 성능을 크게 향상할 수 있습니다.

중간 표현으로 파싱하기

템플릿을 처리하는 코드는 템플릿 문자열을 AST와 같은 중간 표현으로 파싱할 수 있습니다. 많은 템플릿 처리 라이브러리가 이 접근 방식을 사용할 것으로 예상합니다.

예를 들어 이론적인 html() 함수는 str를 반환하는 대신, Motivation 섹션에서 설명한 것처럼 같은 패키지에 정의된 HTML Element를 반환할 수 있습니다.

@dataclass(frozen=True)
class Element:
    tag: str
    attributes: Mapping[str, str | bool]
    children: Sequence[str | Element]

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


def html(template: Template) -> Element:
    ...

그러면 str(element)을 호출하면 HTML이 렌더링되지만, 그동안 Element는 다양한 방식으로 조작할 수 있습니다.

보간의 컨텍스트에 따른 처리

가상의 html() 함수를 계속 살펴보면, 이 함수를 컨텍스트에 따라 동작하도록 만들 수 있습니다. 보간은 템플릿에서 나타나는 위치에 따라 다르게 처리할 수 있습니다.

예를 들어, html() 함수가 여러 종류의 보간을 지원하도록 만들 수 있습니다.

attributes = {"id": "main"}
attribute_value = "shrubbery"
content = "hello"
template = t"<div {attributes} data-value={attribute_value}>{content}</div>"
element = html(template)
assert str(element) == '<div id="main" data-value="shrubbery">hello</div>'

{attributes} 보간은 HTML 태그의 컨텍스트에서 발생하고 이에 대응하는 속성 이름이 없으므로, 속성의 딕셔너리로 처리합니다. {attribute_value} 보간은 단순한 문자열 값으로 처리하며 최종 문자열에 포함하기 전에 인용 부호로 묶습니다. {content} 보간은 안전하지 않을 가능성이 있는 콘텐츠로 처리하며 최종 문자열에 포함하기 전에 이스케이프합니다.

중첩된 템플릿 문자열

html() 함수를 한 단계 더 발전시키면 중첩된 템플릿 문자열을 지원할 수 있습니다. 이를 통해 더 단순한 템플릿으로부터 더 복잡한 HTML 구조를 구성할 수 있습니다.

name = "World"
content = html(t"<p>Hello {name}</p>")
template = t"<div>{content}</div>"
element = html(template)
assert str(element) == '<div><p>Hello World</p></div>'

{content} 보간은 Element의 인스턴스이므로 최종 문자열에 포함하기 전에 이스케이프할 필요가 없습니다.

다음과 같은 편리한 단순화를 생각해 볼 수 있습니다. html() 함수에 Template인스턴스가 전달되면, 중첩된 템플릿에서 자신을 재귀적으로 호출하여 이를 자동으로 Element로 변환할 수 있습니다.

템플릿의 중첩과 조합이 템플릿 처리 코드에서 일반적인 패턴이 될 것이며, 적절한 경우 단순한 문자열 연결보다 우선하여 사용될 것으로 예상합니다.

지연 평가 접근 방식

f-문자열과 마찬가지로 t-문자열 리터럴의 보간은 즉시 평가됩니다. 그러나 지연 평가가 바람직할 수 있는 경우도 있습니다.

단일 보간을 평가하는 데 비용이 많이 든다면 템플릿 문자열 리터럴에서 해당 보간을 lambda 로 명시적으로 감쌀 수 있습니다.

name = "World"
template = t"Hello {(lambda: name)}"
assert callable(template.interpolations[0].value)
assert template.interpolations[0].value() == "World"

물론 이는 템플릿 처리 코드가 호출 가능 객체인 보간 값을 예상하고 처리한다고 가정합니다. (이터레이터, await 가능 객체 등도 지원하는 방식을 생각해 볼 수 있습니다.) 이는 PEP의 요구 사항은 아니지만 템플릿 처리 코드에서 일반적인 패턴입니다.

일반적으로 커뮤니티가 템플릿 문자열의 보간을 지연 평가하기 위한 모범 사례를 개발하고, 적절한 경우 일반 라이브러리가 템플릿 처리 코드에서 호출 가능 객체 또는 await 가능 값을 지원하기를 기대합니다.

비동기 평가 접근 방식

지연 평가와 밀접하게 관련된 것이 비동기 평가입니다.

f-문자열과 마찬가지로 보간에서 await키워드를 사용할 수 있습니다.

async def example():
    async def get_name() -> str:
        await asyncio.sleep(1)
        return "Sleepy"

    template = t"Hello {await get_name()}"
    # Use the f() function from the f-string example, above
    assert f(template) == "Hello Sleepy"

더 정교한 템플릿 처리 코드는 이를 활용하여 보간에서 비동기 작업을 수행할 수 있습니다. 예를 들어, “스마트” 처리 함수는 보간이 await 가능한 객체임을 예상하고 템플릿 문자열을 처리하기 전에 이를 await할 수 있습니다:

async def example():
    async def get_name() -> str:
        await asyncio.sleep(1)
        return "Sleepy"

    template = t"Hello {get_name}"
    assert await async_f(template) == "Hello Sleepy"

이는 async_f()의 템플릿 처리 코드가 비동기적이며 보간(interpolation) 값을 await할 수 있다고 가정합니다.

Note

예제 코드

afstring.pytest_afstring.py를 참조하십시오.

템플릿 재사용 방법

개발자가 서로 다른 값으로 템플릿 문자열을 여러 번 재사용하려는 경우, Template 인스턴스를 반환하는 함수를 작성할 수 있습니다.

def reusable(name: str, question: str) -> Template:
    return t"Hello {name}, {question}?"

template = reusable("friend", "how are you")
template = reusable("King Arthur", "what is your quest")

물론 이는 f-문자열을 재사용하는 방법과 다르지 않습니다.

형식 문자열과의 관계

유서 깊은 str.format() 메서드는 나중에 값을 형식화하는 데 사용할 수 있는 형식 문자열을 받아들입니다.

alas_fmt = "We're all out of {cheese}."
assert alas_fmt.format(cheese="Red Leicester") == "We're all out of Red Leicester."

조금 다르게 보면 형식 문자열을 일종의 함수 정의로 생각할 수 있습니다. 호출str.format() 에 대한 일종의 함수 호출로 볼 수 있습니다. t-문자열에 해당하는 방식은 Template 인스턴스를 반환하는 표준 Python 함수를 간단히 정의하는 것입니다.

def make_template(*, cheese: str) -> Template:
    return t"We're all out of {cheese}."

template = make_template(cheese="Red Leicester")
# Using the f() function from the f-string example, above
assert f(template) == "We're all out of Red Leicester."

make_template() 함수 자체는 형식 문자열과 유사한 것으로 생각할 수 있습니다. make_template()의 호출은 str.format() 호출과 유사합니다.

물론 파일 시스템이나 데이터베이스 같은 외부 소스에서 형식 문자열을 불러오는 것은 일반적입니다. 다행히 TemplateInterpolation은 단순한 Python 타입이므로, 이전 형식의 형식 문자열을 받아 이에 상응하는 Template 인스턴스를 반환하는 함수를 작성할 수 있습니다.

def from_format(fmt: str, /, *args: object, **kwargs: object) -> Template:
     """Parse `fmt` and return a `Template` instance."""
     ...

 # Load this from a file, database, etc.
 fmt = "We're all out of {cheese}."
 template = from_format(fmt, cheese="Red Leicester")
 # Using the f() function from the f-string example, above
 assert f(template) == "We're all out of Red Leicester."

이는 개발자가 이전에는 형식 문자열을 사용했을 수 있는 곳에서 템플릿 문자열을 사용할 수 있도록 하는 강력한 패턴입니다. from_format()의 전체 구현은 예제 저장소에서 제공되며, 형식 문자열의 전체 문법을 지원합니다.

Note

예제 코드

format.pytest_format.py를 참조하십시오.

참조 구현

PEP 750의 CPython 구현은 여기에서 제공됩니다.

참조 구현을 중심으로 구축된 예제와 테스트 공개 저장소도 있습니다. 템플릿 문자열을 사용해 보고 싶다면 이 저장소에서 시작하는 것이 좋습니다.

거부된 아이디어

이 PEP는 여러 차례 중요한 개정을 거쳤습니다. 또한 PEP 501의 개정판과 Discourse 토론에서 모두 상당히 흥미로운 여러 아이디어가 검토되었습니다.

검토되었지만 거부된 가장 중요한 아이디어를 문서화하고자 합니다.

임의의 문자열 리터럴 접두사

JavaScript 태그가 지정된 템플릿 리터럴에서 영감을 받아, 이 PEP의 이전 버전에서는 리터럴 문자열 앞에 임의의 “태그” 접두사를 허용했습니다:

my_tag'Hello {name}'

이 접두사는 “태그 함수”라고 하는 특수한 호출 가능 객체입니다. 태그 함수는 인자 목록으로 템플릿 문자열의 각 부분을 받았습니다. 그런 다음 문자열을 처리하고 임의의 값을 반환할 수 있었습니다:

def my_tag(*args: str | Interpolation) -> Any:
    ...

이 접근 방식은 여러 가지 이유로 거부되었습니다:

  • 이를 완전히 일반적인 방식으로 구축하는 것은 너무 복잡하다고 판단되었습니다. JavaScript에서는 임의의 표현식이 템플릿 문자열 앞에 올 수 있으며, 이는 Python에서 구현하기에 상당한 난제입니다.
  • 이는 향후 새로운 문자열 접두사를 도입하는 것을 막았습니다.
  • 네임스페이스를 불필요하게 오염시키는 것처럼 보였습니다.

단일 t접두사를 사용하는 것이 더 단순하고 Python다운 접근 방식이며, 템플릿 문자열이 f-문자열의 일반화라는 역할에도 더 잘 부합한다고 판단되었습니다.

보간의 지연 평가

이 PEP의 초기 버전에서는 보간을 느리게 평가해야 한다고 제안했습니다. 모든 보간은 암시적 람다로 “래핑”되었습니다. 보간에는 즉시 평가되는 value속성 대신 보간의 값을 확인하는 getvalue()메서드가 있었습니다:

class Interpolation:
    ...
    _value: Callable[[], object]

    def getvalue(self) -> object:
        return self._value()

이는 여러 가지 이유로 거부되었습니다:

  • 템플릿 문자열의 사용 사례 중 압도적인 다수는 자연스럽게 즉시 평가를 요구합니다.
  • 지연 평가는 f-문자열의 동작에서 크게 벗어나는 것이 됩니다.
  • 암시적 람다 래핑은 타입 힌트와 정적 분석에 어려움을 초래합니다.

가장 중요한 점은 지연 평가가 필요한 많은 경우에 암시적 람다 래핑을 대신할 실행 가능한 대안이 있다는 것입니다(완벽하지는 않더라도). 자세한 내용은 위의 Approaches to Lazy Evaluation 섹션을 참조하십시오.

PEP에서는 지연 평가를 거부했지만, 커뮤니티에서 이 아이디어를 계속 탐구하기를 바랍니다.

TemplateInterpolation을 프로토콜로 만들기

이 PEP의 초기 버전에서는 TemplateInterpolation타입을 클래스가 아니라 런타임 검사 가능한 프로토콜로 만들 것을 제안했습니다.

결국에는 클래스를 사용하는 것이 더 간단하다고 판단했습니다.

TemplateInterpolation을 위한 재정의된 __eq____hash__

이 PEP의 이전 버전에서는 TemplateInterpolation타입이 __eq____hash__의 자체 구현을 가져야 한다고 제안했습니다.

Templates는 해당 stringsinterpolations가 같으면 동일한 것으로 간주했고, Interpolations는 해당 value, expression, conversionformat_spec이 같으면 동일한 것으로 간주했습니다. 보간 해싱은 튜플 해싱과 유사했습니다. 즉, Interpolation은 해당 value가 해시 가능할 때 그리고 그 경우에만 해시 가능했습니다.

이렇게 정의된 Template.__hash__는 템플릿 처리 코드에서 캐시 키로 유용하지 않았기 때문에 이를 거부했습니다. 개발자에게 혼란을 줄 수 있다는 점을 우려했습니다.

이러한 __eq____hash__구현을 삭제하면 다음과 같은 assert를 작성할 수 없게 됩니다:

name = "World"
assert t"Hello " + t"{name}" == t"Hello {name}"

Template 인스턴스는 후속 코드에서 신속하게 처리되도록 설계되었으므로 이러한 assert의 유용성은 제한적이라고 판단했습니다.

추가 Decoded 타입

이 PEP의 초기 버전에서는 템플릿 문자열의 “정적 문자열” 부분을 나타내기 위한 추가 타입인 Decoded를 제안했습니다. 이 타입은 str을 상속했으며 문자열의 원본 텍스트를 제공하는 단일 추가 raw속성을 가졌습니다. 이를 거부하고 일반 str을 사용하면서 rt 접두사를 조합할 수 있도록 하는 더 간단한 접근 방식을 채택했습니다.

TemplateInterpolation의 최종 위치

이 PEP의 이전 버전에서는 TemplateInterpolation 타입을 types, collections, collections.abc, 심지어 새로운 최상위 모듈인 templatelib에 배치할 것을 제안했습니다. 최종 결정은 이를 string.templatelib에 배치하는 것이었습니다.

원본 템플릿 리터럴의 완전한 재구성 활성화

이 PEP의 이전 버전에서는 Template인스턴스에서 원본 템플릿 문자열의 텍스트를 완전히 재구성할 수 있도록 하는 방안을 시도했습니다. 이는 지나치게 복잡하다는 이유로 거부되었습니다. 템플릿 리터럴 소스와 기반 AST 사이의 매핑은 일대일이 아니며, 원본 소스 텍스트로의 라운드트립과 관련하여 몇 가지 제한 사항이 있습니다.

먼저, Interpolation.format_spec은 제공되지 않은 경우 ""로 기본 설정됩니다.

value = 42
template1 = t"{value}"
template2 = t"{value:}"
assert template1.interpolations[0].format_spec == ""
assert template2.interpolations[0].format_spec == ""

다음으로 디버그 지정자인 =은 특수한 경우로 처리되며 AST가 생성되기 전에 처리됩니다. 따라서 t"{value=}"t"value={value!r}"를 구별할 수 없습니다.

value = 42
template1 = t"{value=}"
template2 = t"value={value!r}"
assert template1.strings[0] == "value="
assert template1.interpolations[0].expression == "value"
assert template1.interpolations[0].conversion == "r"
assert template2.strings[0] == "value="
assert template2.interpolations[0].expression == "value"
assert template2.interpolations[0].conversion == "r"

마지막으로 f-문자열의 형식 지정자는 임의로 중첩될 수 있습니다. 이 PEP와 참조 구현에서는 Interpolationformat_spec을 설정하기 위해 지정자를 즉시 평가하므로 원래 표현식이 손실됩니다. 예를 들면 다음과 같습니다.

value = 42
precision = 2
template1 = t"{value:.2f}"
template2 = t"{value:.{precision}f}"
assert template1.interpolations[0].format_spec == ".2f"
assert template2.interpolations[0].format_spec == ".2f"

이러한 제한 사항이 실제 사용에서 중요한 문제가 될 것으로 예상하지 않습니다. 원본 템플릿 문자열 리터럴을 가져와야 하는 개발자는 언제든지 inspect.getsource()나 유사한 도구를 사용할 수 있습니다.

Template 연결 금지

이 PEP의 이전 버전에서는 Template인스턴스가 연결을 지원하지 않아야 한다고 제안했습니다. 이는 여러 Template인스턴스를 연결할 수 있도록 하는 방향으로 거부되었습니다.

연결의 일부 또는 모든 형식을 거부하는 데에는 타당한 근거가 있습니다. 즉, 잠재적인 버그의 한 부류를 차단한다는 것입니다. 특히 템플릿 문자열에 복잡한 문법이 포함되는 경우가 많고 연결이 항상 동일한 의미(또는 어떤 의미도)를 갖지는 않는다고 보는 경우에는 더욱 그렇습니다.

더욱이 이 PEP의 가장 초기 버전에서는 JavaScript의 태그가 지정된 템플릿 리터럴과 더 유사한 구문을 제안했으며, 여기서는 임의의 호출 가능 객체를 문자열 리터럴의 접두사로 사용할 수 있었습니다. 해당 호출 가능 객체가 연결을 지원하는 타입을 반환한다는 보장은 없었습니다.

결국 새로운 문자열 타입이 연결을 지원하지 않는다는 사실이 개발자에게 주는 의외성이 연결을 지원함으로써 발생하는 이론적 해보다 클 가능성이 높다고 판단했습니다.

이 PEP에서는 두 Templates의 연결을 지원하지만, Templatestr의 연결은 지원하지 않습니다. 이는 str을 정적 문자열로 취급해야 하는지 보간으로 취급해야 하는지가 모호하기 때문입니다. 개발자는 위에서 설명한 대로 str을 다른 Template과 연결하기 전에 이를 Template인스턴스로 감싸야 합니다.

템플릿 문자열을 사용하는 코드는 연결보다는 중첩과 조합을 통해 더 큰 템플릿을 구성하는 경우가 더 많을 것으로 예상합니다.

임의의 변환 값

Python에서는 가능한 변환 타입 값으로 r, s, a만 허용합니다. 다른 값을 할당하려고 하면 SyntaxError가 발생합니다.

이론적으로 템플릿 함수는 다른 변환 유형을 처리하도록 선택할 수도 있습니다. 그러나 이 PEP는 PEP 701을 밀접하게 따릅니다. 허용되는 값에 대한 변경 사항은 별도의 PEP에서 다루어야 합니다.

Interpolation에서 conversion 제거

이 PEP를 작성하는 동안 Interpolation에서 conversion 속성을 제거하고, Interpolation.value가 설정되기 전에 변환을 즉시 수행하도록 명시하는 방안을 고려했습니다.

이는 템플릿 처리 코드를 작성하는 작업을 단순화하기 위한 것이었습니다. conversion 속성은 확장성이 제한적입니다(Literal["r", "s", "a"] | None으로 타입이 지정됩니다). 사용자 지정 형식 지정자로 더 효과적으로 달성할 수 없는 상당한 가치나 유연성을 이 속성이 템플릿 문자열에 추가하는지는 분명하지 않습니다. 형식 지정자와 달리 Python의 format() 내장 함수에 해당하는 것은 없습니다. (대신 Examples 섹션에 convert()의 예시 구현을 포함합니다.)

결국 f-문자열과의 호환성을 유지하고 향후 확장성을 허용하기 위해 Interpolation 타입에 conversion 속성을 유지하기로 결정했습니다.

대체 보간 기호

이 PEP의 초기 단계에서 템플릿 문자열의 보간에 대체 기호를 허용하는 방안을 고려했습니다. 예를 들어, 국제화(i18n) 또는 기타 목적에 유용할 수 있다는 생각에서 ${name}{name}의 대안으로 허용하는 방안을 고려했습니다. 자세한 내용은 Discourse thread에서 확인하십시오.

이는 t-문자열 구문을 가능한 한 f-문자열 구문에 가깝게 유지하는 방향으로 거부되었습니다.

Template의 대체 레이아웃

이 PEP를 개발하는 동안 Template 타입을 위한 여러 대체 레이아웃을 고려했습니다. 많은 방안은 문자열과 보간을 모두 포함하는 단일 args 튜플에 초점을 맞추었습니다. 다음과 같은 변형이 포함되었습니다.

  • args는 첫 번째와 마지막 항목이 문자열이고 문자열과 보간이 항상 번갈아 나타난다는 약속이 있는 tuple[str | Interpolation, ...]`이었습니다. 이는 args가 항상 비어 있지 않고 인접한 보간 사이에 빈 문자열이 삽입된다는 것을 의미했습니다. 교대 구조를 타입 시스템으로 표현할 수 없었고 이를 보장하기를 원하지 않았기 때문에 거부되었습니다.
  • argstuple[str | Interpolation, ...]로 유지되었지만 인터리빙은 지원하지 않았습니다. 그 결과 시퀀스에 빈 문자열이 추가되지 않았습니다. 더 이상 args[::2]를 사용하여 정적 문자열을 가져올 수 없었으며, 대신 문자열과 보간을 구별하려면 인스턴스 검사 또는 구조적 패턴 매칭을 사용해야 했습니다. 이 접근 방식은 향후 성능 최적화의 가능성이 더 적다는 이유로 거부되었습니다.
  • argsSequence[tuple[str, Interpolation | None]]으로 타입이 지정되었습니다. 각 정적 문자열은 인접한 보간과 쌍을 이루었습니다. 마지막 문자열 부분에는 이에 대응하는 보간이 없었습니다. 지나치게 복잡하다는 이유로 거부되었습니다.

템플릿의 “종류”를 설명하는 메커니즘

t-문자열이 인기를 얻는다면, 템플릿 문자열에서 발견되는 콘텐츠의 “종류”(“sql”, “html”, “css” 등)를 설명하는 방법이 유용할 수 있습니다. 이를 통해 린터, 포매터, 타입 검사기, IDE와 같은 도구에 강력한 새로운 기능을 추가할 수 있습니다. (예를 들어, black이 t-문자열의 HTML 형식을 지정하거나 mypy가 주어진 속성이 HTML 태그에 유효한지 검사하는 것을 상상해 보십시오.) 흥미롭지만, 이 PEP에서는 어떠한 구체적인 메커니즘도 제안하지 않습니다. 시간이 지남에 따라 커뮤니티가 이를 위한 관례를 개발하기를 바랍니다.

바이너리 템플릿 문자열

t-문자열과 바이트(tb)의 결합은 이 PEP의 범위에 포함되지 않는 것으로 간주합니다. 그러나 f-문자열과 달리, t-문자열과 바이트를 결합할 수 없는 근본적인 이유는 없습니다. 향후 PEP에서 지원을 고려할 수 있습니다.

감사의 말

템플릿 문자열로 이어지는 아이디어를 개발하는 동안 기여해 주신 Ryan Morshead에게 감사드립니다. 수년 전에 유사한 아이디어를 다룬 Dropbox의 pyxl에 대해서도 특별히 언급하고자 합니다. Andrea Giammarchi는 이 PEP의 초기 초안에 사려 깊은 의견을 제공했습니다. 마지막으로, tagged library를 선구적으로 작업한 Joachim Viide에게 감사드립니다. Tagged는 템플릿 문자열의 전신일 뿐 아니라, GitHub 이슈 댓글을 통해 전체 작업이 시작된 곳이기도 합니다!