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

PEP 철회

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

October 2024 updatesPEP 750에서 템플릿 문자열이 더 폭넓은 태그 문자열 개념보다 Python에 더 적합하다는 데 동의했습니다.

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

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

Important

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

다른 PEP와의 관계

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

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

이 PEP는 PEP 750의 태그 문자열 제안의 일부 측면과 경쟁합니다(특히 템플릿 렌더링을 render(t"template literal")로 표현할지 아니면 render"template literal"로 표현할지에 관해 그러합니다). 그러나 공통 기능도 many가지 공유합니다(PEP 750이 게시된 후 이 PEP는 태그 문자열 제안에서 영감을 받은 몇 가지 새로운 변경 사항으로 업데이트되었습니다).

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

동기

PEP 498은 컴파일러에 투명한 문자열 보간을 위한 새로운 구문 지원을 추가하여, 보간 연산의 이름 참조가 명시적인 이름 참조로 제한되지 않고 다른 표현식과 마찬가지로 포함하는 네임스페이스에 완전히 접근할 수 있도록 합니다. PEP(및 기타 문서)에서는 이를 “f-string”(“formatted strings”를 연상시키는 이름)이라고 부릅니다.

관련 PEP 498이 승인된 이래 f-문자열은 확고히 자리 잡았으며 큰 인기를 얻었습니다. PEP 701에서 문법이 공식화되면서 f-문자열은 더욱 유용하고 유연해졌습니다. f-string은 훌륭하지만 즉시 렌더링에는 한계가 있습니다. 예를 들어 f-string의 즉시성 때문에 다음과 같은 코드가 안타깝게도 그럴듯해졌습니다.

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

이러한 종류의 코드는 겉보기에는 우아하지만, 보간된 값 message_from_user가 실제로 신뢰할 수 없는 사용자가 제공한 것이라면 중대한 문제가 발생합니다. 제공된 사용자 데이터가 os.system 호출에 전달되기 전에 적절히 이스케이프되지 않아 코드 인젝션 공격의 한 형태가 발생할 수 있기 때문입니다.

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

이 문제(및 여러 다른 우려 사항)를 해결하기 위해 이 PEP는 “t-string”(“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와 이에 의존하는 상위 수준 API(예: subprocess.run())는 보간 템플릿을 받아들이고 새로운 shlex.sh 렌더러에 따라 처리하도록 업데이트됩니다.

배경

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

그 이후 f-문자열은 매우 인기를 얻었으며, 구문과 의미 체계의 일부 미완성된 부분과 제한 사항을 정리하기 위해 PEP 701이 도입되었습니다. 템플릿 리터럴 제안은 f-문자열에 대한 당시의 지식과 PEP 701의 개선 사항을 반영하도록 2023년에 갱신되었습니다.

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

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

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

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

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

태그 문자열이 처음 제안되었을 때는, 렌더링 함수 호출을 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-string(PEP 498)은 Python의 어휘 네임스페이스 의미 체계에 완전히 접근하면서 값을 문자열에 보간하는 작업을 간소화했지만, 그 대가로 SQL 쿼리, 셸 명령 및 HTML 템플릿과 같은 민감한 대상에 값을 보간할 때 코드 삽입 공격을 고려하지 않고 처리하는 경우가 올바르게 처리하는 경우보다 훨씬 깔끔한 구문을 사용하게 되는 상황을 만들었습니다.

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

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

이 제안을 개발한 최초의 동기는 아니었지만, PEP 750에 설명된 도메인별 언어 정의의 많은 이점도 이 PEP에 적용됩니다(선언된 템플릿 변수와 렌더링 함수 매개변수의 타입 사양을 기반으로 코드 편집기에서 DSL별 의미 강조를 제공할 가능성도 포함합니다).

사양

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

템플릿 리터럴은 유니코드 문자열입니다(바이트 리터럴은 허용되지 않음). 문자열 리터럴 연결은 일반적인 방식으로 작동하며, 결합된 전체 리터럴이 템플릿 리터럴을 구성합니다.

템플릿 문자열은 PEP 498PEP 701의 f-string에 대해 설명된 방식에 따라 리터럴, 표현식, 형식 지정자 및 변환 지정자로 구문 분석됩니다. 변환 지정자의 구문은 유효한 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-string으로 즉시 평가됨), 변환 지정자 텍스트(일반 문자열)를 지정하는 구문 분석된 템플릿 보간 필드

원시 템플릿은 단순히 문자열로 표현된 템플릿 리터럴입니다. 기본적으로 이는 템플릿 리터럴을 사람이 읽을 수 있는 형태로 표현하는 데 사용되지만, 템플릿 렌더러는 다른 목적(예: 캐시 조회 키)에도 사용할 수 있습니다.

구문 분석된 템플릿 구조는 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-string 치환 필드를 포함하도록 허용하는 것도 포함됩니다. 필드 지정자의 원시 텍스트(치환 필드를 처리하지 않은 상태)는 전체 원시 템플릿 문자열의 일부로 유지됩니다.

구문 분석된 필드 지정자에는 해당 치환이 이미 해결된 필드 지정자 문자열이 전달됩니다. : 접두사도 생략됩니다.

구문 분석 중 치환 표현식에서 분리되는 것을 제외하면, 서식 지정자는 보간 템플릿 구문 분석기에서 그 밖에는 불투명한 문자열로 처리됩니다. 이러한 지정자에 의미를 부여하는 작업(또는 반대로 해당 지정자의 사용을 금지하는 작업)은 렌더링 시점에 필드 렌더러가 처리합니다.

변환 지정자

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-문자열 자체는 새로운 !() 변환 지정자를 지원하지 않습니다(값 보간과 값 렌더링이 항상 동시에 발생하므로 중복되기 때문입니다). 또한 사용자 지정 변환 지정자의 사용도 지원하지 않습니다(렌더링 함수가 컴파일 시점에 알려져 있으며 사용자 지정 지정자를 사용하지 않기 때문입니다).

관련 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 query object를 생성하는 sqlalchemy.sql.text 호출이 포함될 수 있습니다. 서브프로세스 호출과 관련된 템플릿 렌더러는 subprocess.run에 전달하기에 적합한 문자열 시퀀스를 생성하거나, subprocess.run을 직접 호출하고 그 결과를 반환할 수도 있습니다.

render_textrender_field에서도 문자열이 아닌 값을 반환할 수 있으며, 단 render_template 구현이 그러한 동작을 기대해야 합니다.

관련 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>")

os.system을 실행하거나 subprocess 모듈 API를 사용할 때 시스템 셸을 활성화할 때 발생하는 상당한 위험 없이 외부 프로그램에 액세스하는 데 더 POSIX 셸에 가까운 방식을 제공하는 것을 목표로, shlex모듈에 렌더러를 포함할 것을 제안합니다. 이 렌더러는 Julia programming language에서 제공하는 인터페이스에서 영감을 받은 외부 프로그램 실행 인터페이스를 제공합니다. 단, 백틱 기반 \`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 문서의 기존 주의 사항을 변경하지 않으며, 더 높은 수준의 subprocess API에 관한 os.system() 문서의 참조도 변경하지 않습니다.

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.Popenshell=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는 TemplateLiteralTemplateLiteralText에 대한 반복 및 연결 의미를 명시적으로 정의합니다. 엄밀히 필요하지는 않지만, 이러한 의미를 정의하면 역사적으로 일반 문자열만 지원해 온 코드에서 해당 타입을 더 쉽게 사용할 수 있을 것으로 예상됩니다.

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

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

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

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

관련 operator.convert_field()를 추가하고 내장 format() 함수를 업데이트하는 일은 곧 기본 변환 지정자를 받아들이려는 렌더링 함수 구현에 적절한 지원을 제공하는 문제였습니다.

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

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

t-문자열은 이미 컴파일될 때 !()를 허용하도록 업데이트되고 있으므로, 변환 지정자를 개별 객체의 형식 지정과 관련된 형식 지정자와 유사하게 렌더링 함수와 관련된 것으로 취급하는 것이 타당합니다. 파싱상의 이유로 제외되는 일부 문자를 제외하면, 이는 의미가 이를 소비하는 함수나 객체에 의해 결정되는 자유 텍스트 필드입니다. 이렇게 하면 템플릿의 형식 지정자에 렌더러별 메타 형식을 도입하고 싶은 유혹이 줄어듭니다(렌더러별 정보는 대신 변환 지정자에 배치할 수 있기 때문입니다).

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

이 PEP와 PEP 750의 주요 차이점은 후자가 다른 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로 미루는 것이 더 낫다고 판단합니다.

가능한 로깅 통합에 대한 고려 연기

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

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

이 초기 PEP의 범위를 벗어나지만, 템플릿 리터럴 지원을 로깅 모듈의 이벤트 보고 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.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(국제화) 번역을 위한 더 깔끔한 구문을 제공하는 것이었습니다. 이를 위해서는 수정되지 않은 원본 템플릿에 접근해야 하기 때문입니다. 따라서 Python의 string.Template 형식 지정 및 Mozilla의 l20n 프로젝트에서 사용되는 치환 구문과의 호환성에 초점을 맞추었습니다.

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

이러한 사실을 깨닫고 PEP는 원래 PEP 3101에서 정의되었으며 이후 PEP 498의 기반으로 사용된 str.format() 치환 구문을 사용하도록 변경되었습니다.

이론적으로는 네이티브 템플릿 리터럴에서 인스턴스를 생성할 수 있도록 string.Template을 업데이트하고 구조적 typing.Template 프로토콜을 구현할 수 있지만, PEP 저자들은 그렇게 하는 데 실질적인 이점이 있다고 확인하지 못했습니다.

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

특히 이는 PEP 3101 기반 구문이 아니라 PEP 292 기반 보간 구문을 사용하여 TemplateLiteral 인스턴스를 생성할 수 있는 대체 “t$-문자열” 구문의 가능성을 열어 둡니다:

template = t$”Substitute $words and ${other_values} at runtime”

이러한 방식으로 생성된 템플릿과 일반 t-문자열에서 생성된 템플릿 사이의 런타임상 유일한 차이점은 해당 템플릿의 raw_template 특성 내용에 있습니다.

POSIX가 아닌 셸에 대한 이스케이프된 렌더링 지원 고려 연기

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

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

$ 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의 범위를 벗어납니다.

감사의 말

  • Eric V. 스미스는 PEP 498을 만들고 문자열 보간에서 임의의 표현식 치환이 실현 가능함을 보여 주었습니다.
  • 이 PEP에 영감을 준 태그 문자열의 상당한 설계 개선, 언어 수준의 지연된 템플릿 렌더링 지원의 가치를 널리 알린 활동, 그리고 모든 네이티브 보간 템플릿 지원이 도메인별 언어를 위한 강력한 구문 강조 및 정적 타입 검사 지원을 제공하려는 향후 노력의 견고한 기반을 마련하도록 보장하기 위한 노력에 대해 PEP 750의 저자들에게 감사드립니다.
  • 지연된 렌더링 모델을 i18n 사용 사례에 적용할 수 있는지 탐색하는 데 기여한 Barry Warsaw, Armin Ronacher, Mike Miller에게 감사드립니다(최종적으로는 적어도 Python의 현재 i18n 접근 방식에는 적합하지 않다는 결론에 도달했지만).

참고 자료