Python 字符串格式化

“Python之禅”告诫人们,应该只用一种明确的方式去做某件事。当你发现在Python中有四种字符串格式化的主要方法时,可能会颇感费解。

本节将介绍这四种字符串格式化方法的工作原理以及它们各自的优缺点。除此之外,还会介绍简单的“经验法则”,用来选择最合适的通用字符串格式化方法。

闲话不多说,后续还有很多内容需要讨论。下面用一个简单的示例来实验,假设有以下变量(或常量)可以使用:

>>> errno = 50159747054
>>> name = 'Bob'

基于这些变量,我们希望生成一个输出字符串并显示以下错误消息:

'Hey Bob, there is a 0xbadc0ffee error!'

这个错误可能会在周一早上破坏开发人员的好心情!不过我们的目的是讨论字符串格式化,所以直接开始吧。

Python 字符串格式化 “旧式”字符串格式化

Python内置了一个独特的字符串操作:通过%操作符可以方便快捷地进行位置格式化。如果你在C中使用过printf风格的函数,就会立即明白其工作方式。这里有一个简单的例子:

>>> 'Hello, %s' % name
'Hello, Bob'

这里使用%s格式说明符来告诉Python替换name值的位置。这种方式称为“旧式”字符串格式化。

在旧式字符串格式化中,还有其他用于控制输出字符串的格式说明符。例如,可以将数转换为十六进制符号,或者填充空格以生成特定格式的表格和报告。

下面使用%x格式说明符将int值转换为字符串并将其表示为十六进制数:

>>> '%x' % errno
'badc0ffee'

如果要在单个字符串中进行多次替换,需要对“旧式”字符串格式化语法稍作改动。由于%操作符只接受一个参数,因此需要将字符串包装到右边的元组中,如下所示:

>>> 'Hey %s, there is a 0x%x error!' % (name, errno)
'Hey Bob, there is a 0xbadc0ffee error!'

如果将别名传递给%操作符,还可以在格式字符串中按名称替换变量:

>>> 'Hey %(name)s, there is a 0x%(errno)x error!' % {
...     "name": name, "errno": errno } 'Hey
Bob, there is a 0xbadc0ffee error!'

这种方式能简化格式字符串的维护,将来也容易修改。不必确保字符串值的传递顺序与格式字符串中名称的引用顺序一致。当然,这种技巧的缺点是需要多打点字。

相信你一直在想,为什么将这种printf风格的格式化称为“旧式”字符串格式化。这是因为在技术上有“新式”的格式化方法取代了它,马上就会介绍。尽管“旧式”字符串格式化已经不再受重用,但并未被抛弃,Python的最新版本依然支持。

Python 字符串格式化 “新式”字符串格式化

Python 3引入了一种新的字符串格式化方式,后来又移植到了Python 2.7中。“新式”字符串格式化可以免去%操作符这种特殊语法,并使得字符串格式化的语法更加规整。新式格式化在字符串对象上调用format()函数。

与“旧式”格式化一样,使用format()函数可以执行简单的位置格式化:

>>> 'Hello, {}'.format(name)
'Hello, Bob'

你还可以用别名以任意顺序替换变量。这是一个非常强大的功能,不必修改传递给格式函数的参数就可以重新排列显示顺序:

>>> 'Hey {name}, there is a 0x{errno:x} error!'.format(
...     name=name, errno=errno)
'Hey Bob, there is a 0xbadc0ffee error!'

从上面可以看出,将int变量格式化为十六进制字符串的语法也改变了。现在需要在变量名后面添加:x后缀来传递格式规范。

总体而言,这种字符串格式化语法更加强大,也没有额外增加复杂性。阅读Python文档对字符串格式化语法的描述是值得的。

在Python 3中,这种“新式”字符串格式化比%风格的格式化更受欢迎。但从Python 3.6开始,出现了一种更好的方式来格式化字符串,下一节会详细介绍。

Python 字符串格式化 字符串字面值插值(Python 3.6+)

Python 3.6增加了另一种格式化字符串的方法,称为格式化字符串字面值(formatted string literal)。采用这种方法,可以在字符串常量内使用嵌入的Python表达式。我们通过下面这个简单的示例来体验一下该功能:

>>> f'Hello, {name}!'
'Hello, Bob!'

这种新的格式化语法非常强大。因为其中可以嵌入任意的Python表达式,所以甚至能内联算术运算,如下所示:

>>> a = 5
>>> b = 10
>>> f'Five plus ten is {a + b} and not {2 * (a + b)}.'

'Five plus ten is 15 and not 30.'

本质上,格式化字符串字面值是Python解析器的功能:将f字符串转换为一系列字符串常量和表达式,然后合并起来构建最终的字符串。

假设有如下的greet()函数,其中包含f字符串:

>>> def greet(name, question):
...     return f"Hello, {name}! How's it {question}?"
...

>>> greet('Bob', 'going')
"Hello, Bob! How's it going?"

在剖析函数并明白其本质后,就可以得知函数中的f字符串实际上转换成了类似以下的内容:

>>> def greet(name, question):
...     return ("Hello, " + name + "! How's it " +
            question + "?")

CPython的实际实现比这种方式稍快,因为其中使用BUILD_STRING操作码进行了优化,但两者在功能上是相同的:

>>> import dis
>>> dis.dis(greet)
   2    0 LOAD_CONST       1('Hello, ')
        2 LOAD_FAST        0(name)
        4 FORMAT_VALUE     0
        6 LOAD_CONST       2("! How's it ")
        8 LOAD_FAST        1(question)
       10 FORMAT_VALUE     0
       12 LOAD_CONST       3('?')
       14 BUILD_STRING     5
       16 RETURN_VALUE

字符串字面值也支持str.format()方法所使用的字符串格式化语法,因此可以用相同的方式解决前两节中遇到的格式化问题:

>>> f"Hey {name}, there's a {errno:#x} error!"
"Hey Bob, there's a 0xbadc0ffee error!"

Python新的格式化字符串字面值与ES2015中添加的JavaScript模板字面值(template literal)类似。我认为这对各个语言来说都是一个很好的补充,并且已经开始在Python 3的日常工作中使用。你可以在官方Python文档中了解更多有关格式化字符串字面值的信息。

Python 字符串格式化 模板字符串

Python中的另一种字符串格式化技术是模板字符串(template string)。这种机制相对简单,也不太强大,但在某些情况下可能正是你所需要的。
来看一个简单的问候示例:

>>> from string import Template
>>> t = Template('Hey, $name!')
>>> t.substitute(name=name)
'Hey, Bob!'

从上面可以看到,这里需要从Python的内置字符串模块中导入Template类。模板字符串不是核心语言功能,而是由标准库中的模块提供。

另一个区别是模板字符串不能使用格式说明符。因此,为了让之前的报错字符串示例正常工作,需要手动将int错误码转换为一个十六进制字符串:

>>> templ_string = 'Hey name, there is aerror error!'
>>> Template(templ_string).substitute(
...     name=name, error=hex(errno)) 'Hey
Bob, there is a 0xbadc0ffee error!'

结果不错,但是你可能想知道什么时候应该在Python程序中使用模板字符串。在我看来,最佳使用场景是用来处理程序用户生成的格式字符串。因为模板字符串较为简单,所以是更安全的选择。

其他字符串格式化技术所用的语法更复杂,因而可能会给程序带来安全漏洞。例如,格式字符串可以访问程序中的任意变量。

这意味着,如果恶意用户可以提供格式字符串,那么就可能泄露密钥和其他敏感信息!下面用一个示例来简单演示一下这种攻击方式:

>>> SECRET = 'this-is-a-secret'
>>> class Error:
...     def __init__(self):
...         pass
>>> err = Error()
>>> user_input = '{error.__init__.__globals__[SECRET]}'

# 啊哦……
>> user_input.format(error=err)
'this-is-a-secret'

注意看,假想的攻击者访问格式字符串中的__globals__字典,从中提取了秘密的字符串。吓人吧?用模板字符串就能避免这种攻击。因此,如果处理从用户输入生成的格式字符串,用模板字符串更加安全。

>>> user_input = '${error.__init__.__globals__[SECRET]}'
>>> Template(user_input).substitute(error=err)
ValueError:
"Invalid placeholder in string: line 1, col 1"

Python 字符串格式化 如何选择字符串格式化方法

我完全明白,Python提供的多种字符串格式化方法会让你感到非常困惑。现在或许应该画一些流程图来解释。

但我不打算这样做,而是归纳一个编写Python代码时可以遵循的简单经验法则。

当难以决定选择哪种字符串格式化方法时,可以结合具体情况使用下面这个经验法则。

达恩的Python字符串格式化经验法则

如果格式字符串是用户提供的,使用模板字符串来避免安全问题。如果不是,再考虑Python版本:Python 3.6+使用字符串字面值插值,老版本则使用“新式”字符串格式化。

Python 字符串格式化 关键要点

  • 也许有些令人惊讶,但Python有不止一种字符串格式化的方式。

  • 每种方式都有其优缺点,使用哪一种取决于具体情况。

  • 如果难以选择,可以试试我的字符串格式化经验法则。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程