PEP 501 – 通用模板字面量字串
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>,Nick Humrich <nick at humrich.us>
- 討論郵件列表:
- Discourse 討論主題
- 狀態:
- 草案
- 型別:
- 標準跟蹤
- 依賴:
- 701
- 建立日期:
- 2015年8月8日
- Python 版本:
- 3.12
- 歷史記錄:
- 2015年8月8日,2015年9月5日,2023年3月9日
摘要
儘管 Python f-字串 易於使用且優雅,但當用於構建 shell 命令、SQL 查詢、HTML 片段等時,它們容易受到注入攻擊(例如,os.system(f"echo {message_from_user}"))。本 PEP 引入了模板字面量字串(或“t-字串”),其語法和語義類似於 f-字串,但渲染被延遲到對它們呼叫 format() 或其他模板渲染函式時。這將允許標準庫呼叫、輔助函式和第三方工具安全且智慧地對輸入執行適當的轉義和其他字串處理,同時保留 f-字串的可用性和便利性。
與其他 PEP 的關係
本 PEP 受 PEP 498 中首次實現的 f-字串語法以及 PEP 701 中正式化的語法的啟發,並在此基礎上構建。
本 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-字串”(“格式化字串”的助記符)。
自 PEP 498 被接受以來,f-字串已得到廣泛認可並非常流行。隨著 PEP 701 中正式化的語法,f-字串變得更加有用和靈活。雖然 f-字串很棒,但急切渲染也有其侷限性。例如,f-字串的急切性使得如下程式碼不幸地變得合理
os.system(f"echo {message_from_user}")
這種程式碼表面上很優雅,但如果插值值 message_from_user 實際上是由不受信任的使用者提供的,則會帶來重大問題:它為一種程式碼注入攻擊打開了大門,其中提供的使用者資料在傳遞給 os.system 呼叫之前未正確轉義。
雖然 PEP 675 中引入的 LiteralString 型別註釋意味著型別檢查器能夠針對這種不安全的函式使用報告型別錯誤,但這些錯誤無助於簡化編寫使用更安全替代方案(例如 subprocess.run())的程式碼。
為了解決該問題(以及其他一些問題),本 PEP 提出了補充引入“t-字串”(“模板字面量字串”的助記符),其中 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 shell 命令的模板渲染器
作為延遲渲染支援的好處的實用演示,以及作為其本身的一個有價值的功能,一個新的 sh 模板渲染器將被新增到 shlex 模組中。此渲染器將生成字串,其中所有插值欄位都使用 shlex.quote() 進行轉義。
subprocess.Popen API(以及依賴它的更高級別的 API,例如 subprocess.run())將更新為接受插值模板並根據新的 shlex.sh 渲染器處理它們。
背景
此 PEP 最初被提議作為 PEP 498 的競爭對手。在明確表明急切渲染提案獲得更多立即支援後,它隨後處於延遲狀態數年,等待進一步瞭解 PEP 498 只支援急切渲染而無需額外支援延遲渲染的複雜性的更簡單方法。
從那時起,f-字串變得非常流行,並且引入了 PEP 701 來整理其語法和語義中的一些粗糙邊緣和限制。模板字面量提案在 2023 年進行了更新,以反映當前對 f-字串的瞭解以及 PEP 701 的改進。
2024 年,釋出了 PEP 750,提議了一種用於自定義標記字串字首的通用機制,而不是此 PEP 中更窄的模板字面量提案。此 PEP 再次更新,既包含受標記字串提案啟發的新的想法,又描述了此 PEP 中更窄的模板字面量語法提案相對於更通用的標記字串提案的感知優勢。
與 f-字串的區別總結
f-字串和 t-字串之間的主要區別是
t(模板字面量)字首表示延遲渲染,但在其他方面很大程度上使用了與格式化字串相同的語法和語義- 模板字面量在執行時可用作一種新型別的物件(
types.TemplateLiteral) - 格式化字串使用的預設渲染透過呼叫
format(template)在模板字面量物件上呼叫,而不是在編譯程式碼中隱式完成 - 與 f-字串(在其中轉換說明符在編譯器中直接處理)不同,t-字串轉換說明符在渲染時由渲染函式處理
- 新的
!()轉換說明符表示欄位表示式是一個可呼叫物件,在使用預設的format()渲染函式時應呼叫它。此說明符特別未新增到 f-字串中(因為在那裡沒有意義)。 - 在 t-字串轉換說明符中允許使用第二個
!(任何後續文字都被忽略),作為一種允許自定義模板渲染函式接受自定義轉換說明符而不會破壞預設的TemplateLiteral.render()渲染方法的方法。此功能特別未新增到 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 查詢、shell 命令和 HTML 模板之類的敏感目標時,在不考慮程式碼注入攻擊的情況下處理時將享受更簡潔的語法,而不是在正確處理時享受的語法。
此 PEP 建議提供將模板字面量的實際渲染延遲到格式化字串的其 __format__ 方法的選項,允許透過將模板作為一等物件傳遞來使用其他模板渲染器。
雖然在技術細節上差異很大,但此 PEP 中提出的 types.TemplateLiteral 介面在概念上與 C# 6.0 中引入的本機插值支援的基礎 FormattableString 型別以及 ES6 中引入的JavaScript 模板字面量非常相似。
雖然不是開發該提案的最初動機,但 PEP 750 中描述的定義領域特定語言的許多好處也適用於此 PEP(包括基於已宣告模板變數和渲染函式引數的型別規範在程式碼編輯器中進行每個 DSL 語義突出顯示的潛力)。
規範
此 PEP 提議一個新的 t 字串字首,這將導致建立一個新型別 types.TemplateLiteral 的例項。
模板字面量是 Unicode 字串(不允許使用位元組字面量),並且字串字面量連線按正常方式操作,整個組合字面量形成模板字面量。
模板字串會被解析成文字、表示式、格式說明符和轉換說明符,其描述方式與 PEP 498 和 PEP 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-string 中欄位說明符的語法和處理定義為與 f-string 相同。
這包括允許欄位說明符本身包含 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-string 本身將不支援新的 !() 轉換說明符(因為它在值插值和值渲染始終同時發生時是冗餘的)。它們也不支援使用自定義轉換說明符(因為渲染函式在編譯時已知,並且不使用自定義說明符)。
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模板渲染器可能涉及sqlalchemy.sql.text呼叫,該呼叫生成一個SQL Alchemy查詢物件。與子程序呼叫相關的模板渲染器可以生成適合傳遞給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不涵蓋立即將所有此類渲染器新增到標準庫中(儘管建議使用一個用於shell轉義的渲染器),而是建議確保第三方庫可以輕鬆地提供這些渲染器,並可能在以後的日期合併到標準庫中。
隨著時間的推移,預計處理潛在危險字串輸入的API可能會更新為原生接受插值模板,從而允許透過簡單地將f字串字首替換為t來修復有問題的程式碼示例。
runquery(t"SELECT {column} FROM {table};")
runcommand(t"cat {filename}")
return_response(t"<html><body>{response.body}</body></html>")
建議在shlex模組中包含一個渲染器,旨在為訪問外部程式提供更類似於POSIX shell的體驗,而不會帶來執行os.system或在使用subprocess模組API時啟用系統shell帶來的重大風險。此渲染器將提供一個受Julia程式語言提供的介面啟發的執行外部程式的介面,只是將基於反引號的\`cat $filename\`語法替換為t"cat {filename}"樣式的模板字面量。請參閱新增到shlex的shell轉義渲染器部分中的更多資訊。
錯誤處理
在處理插值表示式時,可能會發生編譯時錯誤或執行時錯誤。編譯時錯誤僅限於在將模板字串解析為其元件元組時可以檢測到的錯誤。這些錯誤都會引發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的shell轉義渲染器
作為參考實現,可以將用於安全POSIX shell轉義的渲染器新增到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不會更改subprocess文件中現有的告誡,即最好避免傳遞shell=True,也不會更改來自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.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 shell渲染器。
這兩個渲染器可以一起用於在學生最初接觸到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() 內建函式,然後是為想要接受預設轉換說明符的渲染函式實現提供適當的支援的問題。
允許自定義渲染器使用任意轉換說明符
接受 !() 作為新的轉換說明符,必然需要更新解析器接受的轉換說明符語法(它們目前僅限於識別符號)。然後提出了一個問題,即 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、系統 shell 和資料庫查詢)或以開發團隊的首選語言(而不是終端使用者的母語)生成應用程式除錯訊息等更簡單的案例。
由於認識到這一點,PEP 決定使用最初在str.format()中定義,並在PEP 3101中首次定義,隨後作為PEP 498的基礎的替換語法。
雖然理論上可以更新string.Template以支援從原生模板字面量建立例項,並實現結構化typing.Template協議,但 PEP 作者尚未發現這樣做有任何實際益處。
但是,此 PEP 中使用的“僅一個字串字首”方法的一個重要好處是,雖然它將現有的 f-字串插值語法推廣到透過 t-字串支援延遲渲染,但它並不意味著這應該是 Python 應該提供的唯一受編譯器支援的插值語法。
最值得注意的是,它為另一種“t$-字串”語法敞開了大門,該語法將允許使用基於PEP 292的插值語法(而不是基於PEP 3101的語法)建立TemplateLiteral例項。
template = t$”Substitute $words and ${other_values} at runtime”
這樣建立的模板與從常規 t-字串建立的模板之間唯一的執行時區別在於其raw_template屬性的內容。
推遲非 POSIX shell 的轉義渲染支援
shlex.quote()的工作原理是將正則表示式字元集[\w@%+=:,./-]分類為安全,並將所有其他字元視為不安全,因此需要對包含它們的字串進行引用。然後,使用的引用機制特定於 POSIX shell 中字串引用的工作方式,因此在執行不遵循 POSIX shell 字串引用規則的 shell 時,無法信任它。
例如,在使用遵循 POSIX 引用規則的 shell 時,執行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)執行 shell 時,仍然不安全。
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 的範圍。
致謝
參考文獻
- %-格式化
- str.format
- string.Template 文件
- PEP 215:字串插值
- PEP 292:更簡單的字串替換
- PEP 3101:高階字串格式化
- PEP 498:文字字串格式化
- PEP 675:任意文字字串型別
- PEP 701:f-字串的語法形式化
- FormattableString 和 C# 原生字串插值
- C# 中的 IFormattable 介面(請參閱備註以獲取全球化說明)
- Javascript 中的 TemplateLiterals
- 在 Julia 中執行外部命令
版權
本文件置於公共領域或根據 CC0-1.0-Universal 許可證,以更寬鬆者為準。
來源:https://github.com/python/peps/blob/main/peps/pep-0501.rst
上次修改時間:2024-09-13 08:53:53 GMT