PEP 3101 – 高级字符串格式化
- 作者:
- Talin <viridia at gmail.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2006年4月16日
- Python 版本:
- 3.0
- 发布历史:
- 2006年4月28日,2006年5月6日,2007年6月10日,2007年8月14日,2008年9月14日
摘要
本PEP提出了一个用于内置字符串格式化操作的新系统,旨在替代现有的“%”字符串格式化运算符。
基本原理
Python 目前提供了两种字符串插值方法
本PEP的主要范围涉及内置字符串格式化操作的提案(换句话说,是内置字符串类型的方法)。
“%”运算符主要受限于它是一个二元运算符,因此最多只能接受两个参数。其中一个参数已用于格式字符串,导致所有其他变量必须挤入剩余的参数中。目前的做法是使用字典或元组作为第二个参数,但许多人评论[3],这种做法缺乏灵活性。“全有或全无”的方法(意味着必须在仅位置参数或仅命名参数之间选择)被认为过于受限。
尽管本提案与 string.Template 之间存在一些重叠,但认为它们各自服务于不同的需求,并且一个并不能取代另一个。本提案旨在提供一种机制,类似于“%”,对于只使用一次的小字符串效率很高,因此,例如,本提案不考虑将字符串编译成模板,尽管本提案确实仔细定义了格式字符串和 API,使得高效的模板包可以重用语法甚至一些底层的格式化代码。
规范
规范将包括以下几个部分
- 内置字符串类中新增格式化方法的规范。
- string 模块中新增函数和标志值的规范,以便底层格式化引擎可以与附加选项一起使用。
- 格式字符串新语法的规范。
- 一组新的特殊方法的规范,用于控制对象的格式化和转换。
- 用户定义格式化类的 API 规范。
- 格式化错误处理的规范。
关于字符串编码的说明:在 Python 3.0 的背景下讨论本 PEP 时,假设所有字符串都是 unicode 字符串,并且本文档中“string”一词通常指的是 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"))
此函数将在后面的章节中描述。
格式字符串
格式字符串由交织的字符数据和标记组成。
字符数据是未经更改地从格式字符串传输到输出字符串的数据;标记不会直接从格式字符串传输到输出,而是用于定义“替换字段”,这些字段向格式引擎描述应在输出字符串中替换标记的位置。
花括号('curly braces')用于指示字符串中的替换字段
"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__ 方法应测试 `specifiers` 参数的类型,以确定是返回字符串还是 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”将导致以参数 0 调用“get_value”。在“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 和 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