PEP 3101 – 高级字符串格式化
- 作者:
- Talin <viridia at gmail.com>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2006-04-16
- Python 版本:
- 3.0
- 历史记录:
- 2006-04-28, 2006-05-06, 2007-06-10, 2007-08-14, 2008-09-14
摘要
此 PEP 提出了一种用于内置字符串格式化操作的新系统,旨在替代现有的“%”字符串格式化运算符。
理由
Python 目前提供两种字符串插值方法
此 PEP 的主要范围涉及针对内置字符串格式化操作的提案(换句话说,内置字符串类型的函数)。
“%”运算符的主要限制在于它是一个二元运算符,因此最多只能接受两个参数。其中一个参数已经用于格式字符串,而所有其他变量都必须挤入剩余的参数。目前的做法是使用字典或元组作为第二个参数,但正如许多人评论的那样 [3],这缺乏灵活性。“全有或全无”的方式(意味着必须在仅使用位置参数或仅使用命名参数之间做出选择)被认为过于限制。
虽然此提案与 string.Template 有些重叠,但人们认为两者各自满足不同的需求,而且一个不会取代另一个。此提案针对一种机制,它与“%”一样,对于仅使用一次的小字符串来说效率很高,因此,例如,将字符串编译成模板在这项提案中并未考虑,尽管该提案确实注意以这样一种方式定义格式字符串和 API,即高效的模板包可以重用语法,甚至可以重用一些底层格式化代码。
规范
规范将包含以下部分
- 指定要添加到内置字符串类中的新格式化方法。
- 指定要添加到 string 模块中的函数和标志值,以便可以将底层格式化引擎与其他选项一起使用。
- 指定格式字符串的新语法。
- 指定一组新的特殊方法来控制对象的格式化和转换。
- 指定用于用户定义的格式化类的 API。
- 指定如何处理格式化错误。
关于字符串编码的说明:在 Python 3.0 的上下文中讨论此 PEP 时,假设所有字符串都是 unicode 字符串,并且在此文档中使用“字符串”一词通常指的是 Python 3.0 字符串,与 Python 2.x 的 unicode 对象相同。
在 Python 2.x 的上下文中,在此文档中使用“字符串”一词指的是一个对象,它可以是普通字符串也可以是 unicode 对象。此 PEP 中描述的所有函数调用接口都可以用于字符串和 unicode 对象,并且在所有情况下,都有足够的信息能够正确推断输出字符串类型(换句话说,无需两个单独的 API)。在所有情况下,格式字符串的类型占主导地位 - 也就是说,转换结果始终会产生一个包含与输入格式字符串相同的字符表示的对象。
字符串方法
内置字符串类(以及 2.6 中的 unicode 类)将获得一个新的方法“format”,它接受任意数量的位置参数和关键字参数
"The story of {0}, {1}, and {c}".format(a, b, c=d)
在格式字符串中,每个位置参数都使用一个从零开始的数字标识,因此在上面的示例中,“a”是参数 0,“b”是参数 1。每个关键字参数都使用其关键字名称标识,因此在上面的示例中,“c”用于引用第三个参数。
还有一个全局内置函数“format”,它格式化单个值
print(format(10.0, "7.3g"))
此函数将在后面的部分中描述。
格式字符串
格式字符串由交织的字符数据和标记组成。
字符数据是从格式字符串到输出字符串不变传输的数据;标记不会直接从格式字符串传输到输出,而是用于定义“替换字段”,描述格式化引擎应该将什么放置在输出字符串中以代替标记。
大括号(“花括号”)用于指示字符串中的替换字段
"My name is {0}".format('Fred')
结果是字符串
"My name is Fred"
大括号可以通过加倍进行转义
"My name is {0} :-{{}}".format('Fred')
这将产生
"My name is Fred :-{}"
大括号内的元素称为“字段”。字段由“字段名”组成,字段名可以是简单字段名,也可以是复合字段名,以及可选的“格式说明符”。
简单和复合字段名
简单字段名是名称或数字。如果为数字,则必须是有效的十进制整数;如果为名称,则必须是有效的 Python 标识符。数字用于标识位置参数,而名称用于标识关键字参数。
复合字段名是表达式中多个简单字段名的组合
"My name is {0.name}".format(open('out.txt', 'w'))
此示例显示了在字段表达式中使用“getattr”或“点”运算符。点运算符允许将输入值的属性指定为字段值。
与某些其他编程语言不同,您不能在格式字符串中嵌入任意表达式。这是有意为之 - 您可以使用的表达式类型是故意限制的。仅支持两种运算符:“.”(getattr)运算符和“[]”(getitem)运算符。允许使用这些运算符的原因是,它们在非病态代码中通常不会产生副作用。
“getitem”语法的示例
"My name is {0[name]}".format(dict(name='Fred'))
应该注意的是,在格式字符串中使用“getitem”比其常规用法要有限得多。在上面的示例中,字符串“name”实际上是文字字符串“name”,而不是名为“name”的变量。用于解析项目键的规则非常简单。如果它以数字开头,则将其视为数字,否则将其视为字符串。
由于键没有用引号分隔,因此无法从格式字符串中指定任意字典键(例如,字符串“10”或“:-]”)。
实现说明:此提案的实现不需要强制执行关于简单或带点的名称是有效的 Python 标识符的规则。相反,它将依赖于底层对象的 getattr 函数来抛出异常,如果标识符不合法。str.format()
函数将具有一个极简的解析器,该解析器只尝试找出何时它“完成”一个标识符(通过找到“.”或“]”或“}”等)。
格式说明符
每个字段还可以指定一组可选的“格式说明符”,这些说明符可用于调整该字段的格式。格式说明符位于字段名之后,用冒号(“:”)字符将两者分隔开
"My name is {0:8}".format('Fred')
格式说明符的含义和语法取决于正在格式化的对象的类型,但有一组标准的格式说明符用于任何不覆盖它们的的对象。
格式说明符本身可以包含替换字段。例如,一个字段的字段宽度本身是一个参数,可以通过以下方式指定
"{0:{1}}".format(a, b)
这些“内部”替换字段只能出现在替换字段的格式说明符部分。内部替换字段本身不能有格式说明符。这意味着替换字段也不能嵌套到任意级别。
注意,末尾的双“}”,它通常会进行转义,在这种情况下没有转义。原因是因为“{{”和“}}”用于转义的语法仅在在格式字段之外使用时才应用。在格式字段内,大括号始终具有其正常含义。
格式说明符的语法是开放式的,因为类可以覆盖标准格式说明符。在这种情况下,str.format()
方法仅将第一个冒号和匹配的大括号之间的所有字符传递给相关的底层格式化方法。
标准格式说明符
如果对象没有定义自己的格式说明符,则使用一组标准的格式说明符。这些在概念上类似于现有“%”运算符使用的格式说明符,但也有许多区别。
标准格式说明符的一般形式为
[[fill]align][sign][#][0][minimumwidth][.precision][type]
方括号 ([]) 表示可选元素。
然后,可选的对齐标志可以是以下之一
'<' - Forces the field to be left-aligned within the available
space (This is the default.)
'>' - Forces the field to be right-aligned within the
available space.
'=' - Forces the padding to be placed after the sign (if any)
but before the digits. This is used for printing fields
in the form '+000000120'. This alignment option is only
valid for numeric types.
'^' - Forces the field to be centered within the available
space.
注意,除非定义了最小字段宽度,否则字段宽度始终与填充它的数据的尺寸相同,因此在这种情况下,对齐选项没有意义。
可选的“填充”字符定义用于将字段填充到最小宽度的字符。如果存在填充字符,则必须后跟一个对齐标志。
“符号”选项仅对数字类型有效,可以是以下之一
'+' - indicates that a sign should be used for both
positive as well as negative numbers
'-' - indicates that a sign should be used only for negative
numbers (this is the default behavior)
' ' - indicates that a leading space should be used on
positive numbers
如果存在“#”字符,则整数使用“备用形式”进行格式化。这意味着二进制、八进制和十六进制输出将分别以“0b”、“0o”和“0x”为前缀。
“宽度”是一个十进制整数,定义最小字段宽度。如果未指定,则字段宽度将由内容确定。
如果宽度字段以零 (“0”) 字符开头,则会启用零填充。这相当于“=”的对齐类型和“0”的填充字符。
“精度”是一个十进制数,指示在浮点转换中应显示小数点后的位数。对于非数字类型,该字段表示最大字段尺寸 - 换句话说,将使用多少个字符来自字段内容。精度对整数转换无效。
最后,“类型”确定应如何显示数据。
可用的整数表示类型为
'b' - Binary. Outputs the number in base 2.
'c' - Character. Converts the integer to the corresponding
Unicode character before printing.
'd' - Decimal Integer. Outputs the number in base 10.
'o' - Octal format. Outputs the number in base 8.
'x' - Hex format. Outputs the number in base 16, using
lower-case letters for the digits above 9.
'X' - Hex format. Outputs the number in base 16, using
upper-case letters for the digits above 9.
'n' - Number. This is the same as 'd', except that it uses the
current locale setting to insert the appropriate
number separator characters.
'' (None) - the same as 'd'
可用的浮点表示类型为
'e' - Exponent notation. Prints the number in scientific
notation using the letter 'e' to indicate the exponent.
'E' - Exponent notation. Same as 'e' except it converts the
number to uppercase.
'f' - Fixed point. Displays the number as a fixed-point
number.
'F' - Fixed point. Same as 'f' except it converts the number
to uppercase.
'g' - General format. This prints the number as a fixed-point
number, unless the number is too large, in which case
it switches to 'e' exponent notation.
'G' - General format. Same as 'g' except switches to 'E'
if the number gets to large.
'n' - Number. This is the same as 'g', except that it uses the
current locale setting to insert the appropriate
number separator characters.
'%' - Percentage. Multiplies the number by 100 and displays
in fixed ('f') format, followed by a percent sign.
'' (None) - similar to 'g', except that it prints at least one
digit after the decimal point.
对象能够定义自己的格式说明符来替换标准格式说明符。例如,“datetime”类,其格式说明符可能类似于strftime()
函数的参数
"Today is: {0:%a %b %d %H:%M:%S %Y}".format(datetime.now())
对于所有内置类型,空格式说明符将产生等效于str(value)
的结果。建议定义自己的格式说明符的对象也遵循此约定。
显式转换标志
显式转换标志用于在格式化字段值之前将其转换。这可以用于覆盖特定于类型的格式化行为,并将值格式化为更通用的类型。目前,识别两个显式转换标志
!r - convert the value to a string using repr().
!s - convert the value to a string using str().
这些标志位于格式说明符之前
"{0!r:20}".format("Hello")
在前面的示例中,将打印字符串“Hello”,带有引号,在一个至少 20 个字符宽的字段中。
自定义 Formatter 类可以定义其他转换标志。如果指定了无效的转换标志,内置格式化程序将引发 ValueError。
控制基于类型的格式化
每个 Python 类型都可以通过定义一个 __format__
方法来控制其实例的格式化方式。 __format__
方法负责解释格式说明符,格式化值并返回结果字符串。
新的全局内置函数“format”只是简单地调用此特殊方法,类似于 len()
和 str()
简单地调用它们各自的特殊方法。
def format(value, format_spec):
return value.__format__(format_spec)
使用“None”值调用此函数是安全的(因为 Python 中的“None”值是一个对象,可以有方法)。
几个内置类型,包括“str”、“int”、“float”和“object”,都定义了 __format__
方法。这意味着,如果从这些类型中的任何一个派生,你的类将知道如何格式化自身。
object.__format__
方法是最简单的:它只是将对象转换为字符串,然后再次调用 format。
class object:
def __format__(self, format_spec):
return format(str(self), format_spec)
“int”和“float”的 __format__
方法将根据格式说明符执行数字格式化。在某些情况下,这些格式化操作可能被委托给其他类型。例如,在“int”格式化程序看到格式类型为“f”(表示“float”)的情况下,它可以简单地将值转换为浮点数,然后再次调用 format()
。
任何类都可以覆盖 __format__
方法,以提供对该类型的自定义格式化。
class AST:
def __format__(self, format_spec):
...
注意 Python 2.x:'format_spec' 参数将是字符串对象或 Unicode 对象,具体取决于原始格式字符串的类型。 __format__
方法应该测试指定参数的类型,以确定是返回字符串还是 Unicode 对象。 __format__
方法有责任返回正确类型的对象。
请注意,上面提到的“显式转换”标志不会传递给 __format__
方法。相反,预期标志指定的转换将在调用 __format__
之前执行。
用户定义的格式化
有时,根据类型自定义字段格式化是不够的。一个例子可能是电子表格应用程序,当值太大而无法容纳在可用空间中时,它会显示井号“#”。
为了获得更强大、更灵活的格式化功能,可以通过“string”模块中的“Formatter”类访问底层格式引擎。此类接受通过普通 str.format 方法无法访问的其他选项。
应用程序可以子类化 Formatter 类以创建自己的自定义格式化行为。
PEP 并没有试图精确地指定 Formatter
类定义的所有方法和属性;相反,这些将在初始实现中定义和记录。但是,此 PEP 将指定对 Formatter
类的通用要求,如下所示。
尽管 string.format()
不直接使用 Formatter
类进行格式化,但两者都使用相同的底层实现。 string.format()
不直接使用 Formatter
类的原因是“string”是一个内置类型,这意味着它的所有方法都必须在 C 中实现,而 Formatter
是一个 Python 类。 Formatter
为与 string.format()
使用的相同 C 函数提供可扩展的包装器。
格式化程序方法
Formatter
类不接受任何初始化参数。
fmt = Formatter()
Formatter
类的公共 API 方法如下:
-- format(format_string, *args, **kwargs)
-- vformat(format_string, args, kwargs)
“format”是主要的 API 方法。它接受一个格式模板和一组任意位置参数和关键字参数。'format' 只是一个调用 'vformat' 的包装器。
“vformat”是执行实际格式化工作的函数。它作为单独的函数公开,用于您想传入预定义的字典参数的情况,而不是使用 *args
和 **kwds
语法将字典解包并重新打包为单个参数。“vformat”的工作是将格式模板字符串分解为字符数据和替换字段。它根据需要调用“get_positional”和“get_index”方法(如下所述)。
Formatter
定义了以下可覆盖方法:
-- get_value(key, args, kwargs)
-- check_unused_args(used_args, args, kwargs)
-- format_field(value, format_spec)
“get_value”用于检索给定的字段值。'key' 参数将是整数或字符串。如果它是整数,则它表示 'args' 中位置参数的索引;如果它是字符串,则它表示 'kwargs' 中的命名参数。
“args”参数设置为 'vformat' 的位置参数列表,“kwargs”参数设置为位置参数的字典。
对于复合字段名称,这些函数仅对字段名称的第一个组件调用;后续组件通过正常的属性和索引操作处理。
例如,字段表达式“0.name”会导致“get_value”使用“key”参数 0 被调用。在“get_value”返回后,将通过调用内置的“getattr”函数查找“name”属性。
如果索引或关键字引用不存在的项目,则应引发 IndexError/KeyError
。
“check_unused_args”用于实现对未使用的参数的检查(如果需要)。此函数的参数是格式字符串中实际引用的所有参数键的集合(位置参数的整数和命名参数的字符串),以及传递给 vformat 的 args 和 kwargs 的引用。未使用的参数集可以从这些参数计算出来。“check_unused_args”假设在检查失败时抛出异常。
“format_field”只是简单地调用全局“format”内置函数。提供此方法是为了让子类可以覆盖它。
为了更好地理解这些函数之间的关系,以下是一段伪代码,解释了 vformat 的一般操作:
def vformat(format_string, args, kwargs):
# Output buffer and set of used args
buffer = StringIO.StringIO()
used_args = set()
# Tokens are either format fields or literal strings
for token in self.parse(format_string):
if is_format_field(token):
# Split the token into field value and format spec
field_spec, _, format_spec = token.partition(":")
# Check for explicit type conversion
explicit, _, field_spec = field_spec.rpartition("!")
# 'first_part' is the part before the first '.' or '['
# Assume that 'get_first_part' returns either an int or
# a string, depending on the syntax.
first_part = get_first_part(field_spec)
value = self.get_value(first_part, args, kwargs)
# Record the fact that we used this arg
used_args.add(first_part)
# Handle [subfield] or .subfield. Assume that 'components'
# returns an iterator of the various subfields, not including
# the first part.
for comp in components(field_spec):
value = resolve_subfield(value, comp)
# Handle explicit type conversion
if explicit == 'r':
value = repr(value)
elif explicit == 's':
value = str(value)
# Call the global 'format' function and write out the converted
# value.
buffer.write(self.format_field(value, format_spec))
else:
buffer.write(token)
self.check_unused_args(used_args, args, kwargs)
return buffer.getvalue()
请注意,Formatter 类的实际算法(将在 C 中实现)可能不是这里介绍的算法。(实际实现很可能根本不是“类”——相反,vformat 可能只是调用一个 C 函数,该函数接受其他可覆盖方法作为参数。)此代码示例的主要目的是说明可覆盖方法调用的顺序。
自定义格式化程序
本节描述了一些可以自定义 Formatter 对象的典型方法。
为了支持备用格式字符串语法,可以覆盖“vformat”方法来更改解析格式字符串的方式。
一个常见的需求是支持“默认”命名空间,这样您就不需要向 format()
方法传递关键字参数,而是可以使用预先存在的命名空间中的值。这可以通过如下覆盖 get_value()
来轻松完成:
class NamespaceFormatter(Formatter):
def __init__(self, namespace={}):
Formatter.__init__(self)
self.namespace = namespace
def get_value(self, key, args, kwds):
if isinstance(key, str):
try:
# Check explicitly passed arguments first
return kwds[key]
except KeyError:
return self.namespace[key]
else:
Formatter.get_value(key, args, kwds)
您可以使用它轻松地创建一个格式化函数,该函数允许访问全局变量,例如:
fmt = NamespaceFormatter(globals())
greeting = "hello"
print(fmt.format("{greeting}, world!"))
可以使用类似的技术通过 locals()
字典访问 locals 字典。
还可以创建一个“智能”命名空间格式化程序,该格式化程序可以通过窥视调用堆栈自动访问 locals 和 globals。由于需要与不同版本的 Python 保持兼容,因此这种功能不会包含在标准库中,但预计有人会创建和发布执行此操作的食谱。
另一种自定义类型是通过覆盖“format_field”方法来更改内置类型格式化的方式。(对于非内置类型,您只需在该类型上定义一个 __format__
特殊方法即可。)例如,您可以覆盖数字的格式化,使其在需要时输出科学记数法。
错误处理
在格式化期间可能发生两类异常:格式化程序代码本身生成的异常和用户代码生成的异常(例如,字段对象的“getattr”函数)。
一般来说,格式化程序代码本身生成的异常属于“ValueError”种类——格式字符串的实际“值”存在错误。(这并不总是正确的;例如,string.format()
函数可能会被传递一个非字符串作为其第一个参数,这将导致 TypeError
)。
与这些内部生成的 ValueError
异常相关的文本将指示格式字符串内异常的位置以及异常的性质。
对于用户代码生成的异常,将向回溯堆栈添加一个跟踪记录和一个虚拟帧,以帮助确定发生异常的字符串位置。插入的回溯将指示错误发生在
File "<format_string>;", line XX, in column_YY
其中 XX 和 YY 分别表示字符串中的行和字符位置信息。
备用语法
自然,最具争议的问题之一是格式字符串的语法,特别是用于指示字段的标记约定。
我不会试图穷尽地列出所有不同的提案,而是会涵盖那些已经最广泛使用的提案。
- Shell 变量语法:
$name
和$(name)
(或在某些变体中,${name}
)。这可能是最古老的约定,由 Perl 和许多其他人使用。当不使用花括号时,变量的长度通过词法扫描直到找到无效字符来确定。此方案通常用于插值隐式的情况——也就是说,在任何字符串都可以包含插值变量的环境中,不需要调用任何特殊的替换函数。在这种情况下,重要的是要防止意外发生插值行为,因此“$”(它在其他情况下是一个相对不常用的字符)用于表示何时应发生此行为。
然而,作者认为,在显式调用格式的情况下,无需过多注意防止意外插值,在这种情况下可以使用更轻量级且更易于使用的语法。
- printf 及其同类('%'),包括添加字段索引的变体,以便可以按顺序插值字段。
- 其他仅带括号的变体。各种 MUD(多用户地下城),例如 MUSH,使用括号(例如
[name]
)进行字符串插值。Microsoft .Net 库使用大括号 ({}
),以及与本提案中语法非常相似的语法,尽管格式说明符的语法却大不相同。[4] - 反引号。此方法的优点是语法杂乱程度最小,但是它缺少函数调用语法的许多优点(例如复杂的表达式参数、自定义格式化程序等)。
- 其他变体包括 Ruby 的
#{}
、PHP 的{$name}
等等。
语法的某些具体方面需要进一步说明。
1) 反斜杠字符用于转义。此 PEP 的原始版本使用反斜杠而不是双重转义来转义括号。之所以有效,是因为 Python 字符串字面量中的反斜杠(不符合标准反斜杠序列,例如 \n
)将保持不变。但是,这引起了某种程度的混淆,并导致了多个递归转义的潜在情况,即 \\\\{
用于在括号前面放置一个文字反斜杠。
2) 使用冒号字符(':')作为格式说明符的分隔符。之所以选择它,仅仅是因为 .Net 使用了它。
备用功能提案
限制属性访问:PEP 的早期版本限制了访问以下划线开头的属性的能力,例如“{0}._private”。但是,在调试时,这是一种有用的功能,因此该功能被放弃了。
一些开发人员建议完全放弃执行 'getattr' 和 'getitem' 访问的能力。但是,这与另一组强烈要求能够将大型字典作为单个参数传递(不使用 **kwargs
语法将其展平为单个关键字参数)然后让格式字符串单独引用字典条目的开发人员的需求相冲突。
还有一些建议扩展格式字符串中允许的表达式集。但是,人们认为这违背了 TOOWTDI 的精神,因为在大多数情况下,可以通过在参数传递到格式化函数之前对其执行相同的表达式来实现相同的效果。对于使用格式字符串在数据丰富的环境中执行任意格式化的用例,建议使用专门为此目的而设计的模板引擎,例如 Genshi [5] 或 Cheetah [6]。
许多其他功能也被考虑过并被拒绝,因为它们可以通过子类化 Formatter
来轻松实现,而不是将该功能构建到基本实现中。这包括备用语法、格式字符串中的注释以及许多其他功能。
安全注意事项
从历史上看,字符串格式化一直是基于 Web 的应用程序中常见安全漏洞的来源,尤其是在字符串格式化系统允许将任意表达式嵌入到格式字符串中的情况下。
以不创建潜在安全漏洞的方式使用字符串格式化的最佳方法是,永远不要使用来自不受信任来源的格式字符串。
如果没有,那么下一个最佳方法是确保字符串格式化没有副作用。由于 Python 的开放性,无法保证任何非平凡操作都具有此属性。此 PEP 的作用是将格式字符串中的表达式类型限制为那些可见副作用既罕见又受到 Python 开发人员文化的强烈反对的类型。因此,例如,允许属性访问,因为将属性访问具有可见副作用的代码写为病态的(无论代码是否具有 **不可见** 副作用 - 例如创建缓存条目以加快查找速度 - 都是无关紧要的)。
示例实现
Patrick Maupin 和 Eric V. Smith 创建了此 PEP 的早期版本的实现,可以在 pep3101 沙盒中找到:
向后兼容性
可以通过保留现有的机制来维护向后兼容性。新系统不会与任何现有字符串格式化技术的任何方法名称冲突,因此这两个系统可以共存,直到需要弃用旧系统。
参考文献
版权
本文档已进入公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-3101.rst
上次修改: 2023-09-09 17:39:29 GMT