Python 的每个新版本都会为语言添加新特性。对于 Python 3.8,最大的变化就是通过:=
操作符,在表达式中间赋值变量提供了一种新语法,这个运算符俗称为海象运算符。本文将解释 Walrus Operator的差别、使用案例、将其与现有方法进行比较并权衡利弊。:)
【注意】本文所有 Walrus Operator 示例都需要 Python 3.8 或更高版本才能运行。
个人博客: https://jianpengzhang.github.io/
CSDN博客: http://blog.csdn.net/u011521019
公众号:滑翔的纸飞机
运算符 :=
的正式名称是赋值表达式运算符(“Assignment Expression Operator”)。在早期的讨论中,它被称为海象运算符,因为 :=
语法很像海象的眼睛和獠牙。也可以将 :=
运算符称为冒号等号运算符。当然,另一个用于赋值表达式的术语是命名表达式。本文统一通过“海象运算符”来称呼。
初步了解海象运算符的作用,请运行以下代码:
In [1]: walrus = False
In [2]: walrus
Out[2]: False
In [3]: (walrus := True)
Out[3]: True
In [4]: walrus
Out[4]: True
第 1 行: 显示了一个传统的赋值语句,将值 False 赋给了 walrus。
第 3 行: 使用海象运算符将值 True 赋给 walrus。
在上面的“walrus”变量中,我们可以看到两种赋值类型之间有一个微妙但重要的区别。海象运算符返回值,而传统赋值不返回值。在第 1 行的 “walrus = False” 之后,没有打印任何值,而在第 3 行的海象运算符表达式之后,则打印出了 True。
从这个例子中,我们可以看到海象运算符一个重要方面。虽然 :=
操作符看起来很新颖,它只是让某些结构更方便,有时还能更清晰地传达代码的意图。
你可能想知道为什么要在第 3 行使用括号,本文稍后会告诉你为什么要使用括号。
现在你对 :=
运算符及其功能有了基本的了解。它是赋值表达式中使用的运算符,与传统的赋值语句不同,它可以返回被赋值的值。要深入了解海象运算符,请继续阅读,看看在哪些地方应该使用它,哪些地方不应该使用它。
与 Python 中的大多数新特性一样,海象运算符是通过 Python 增强提案 (PEP) 引入的。PEP 572 描述了引入海象运算符的动机、语法细节以及 :=
运算符可用于改进代码的示例。
海象运算符由 Emily Morehouse 实现,并在 Python 3.8 的第一个 alpha 版本中发布。
在包括 C 语言及其衍生语言在内的许多语言中,赋值语句都具有表达式的功能。这既可能是非常强大的功能,也可能是令人困惑的错误根源。例如,以下代码是有效的 C 语言,但并不能按预期执行:
int x = 3, y = 8;
if (x = y) {
printf("x and y are equal (x = %d, y = %d)", x, y);
}
在这里,如果 (x = y) 的值为 true,代码片段将打印出:“x and y are equal (x = 8, y = 8)”。这并不是期望的结果,试图比较 x 和 y,那么 x 的值是如何从 3 变为 8 的?
问题在于你使用的是赋值运算符 (=) 而不是相等比较运算符 (==)。在 C 语言中,x = y 是一个求 y 值的表达式。在本例中,x = y 的值为 8,在 if 语句中被认为是真实的。
请看 Python 中的相应示例。这段代码会引发语法错误:
In [1]: x, y = 3, 8
...: if x = y:
...: print(f"x and y are equal ({x = }, {y = })")
Cell In [1], line 2
if x = y:
^
SyntaxError: invalid syntax
与 C 示例不同,这段 Python 代码给出的是一个明确的错误,而不是一个隐含的BUG。
Python 中赋值语句和赋值表达式的区别对于避免这类难以发现的BUG非常有用。
PEP 572 中认为 Python 应该为赋值语句和表达式使用不同的语法,而不是将现有的赋值语句变成表达式。
支持海象运算符的一个设计原则是,在没有相同的代码上下文中,使用 =
运算符的赋值语句和使用 :=
运算符的赋值表达式都是有效的。例如,你不能使用海象运算符进行普通赋值:
In [7]: walrus := True
Cell In [7], line 1
walrus := True
^
SyntaxError: invalid syntax
在许多情况下,你可以在海象运算符周围添加括号 ()
使其成为有效的 Python 表达式:
>>> (walrus := True) # Valid, but regular statements are preferred
True
在这些括号内不允许使用 =
来编写传统的赋值语句。这有助于发现潜在的错误。
In [9]: (walrus = True)
Cell In [9], line 1
(walrus = True)
^
SyntaxError: invalid syntax
本文档稍后将详细介绍不允许使用海象运算符的情况,但首先要了解可能需要使用海象运算符的情况。
在本节中,你将看到几个使用海象运算符简化代码的示例。在所有这些示例中,一个重要的目的就是避免各种重复:
你将看到海象运算符如何在这些情况下提供帮助。
列表是 Python 中强大的数据结构,通常代表一系列相关的属性。同样,字典在 Python 中也被广泛使用,是结构化信息的主要载体。有时,在设置这些数据结构时,会多次执行相同的操作。
示例一:
作为第一个例子,统计一个数字列表并将其存储在字典中:
In [1]: numbers = [2, 8, 0, 1, 1, 9, 7, 7]
In [2]: description = {
...: "length": len(numbers),
...: "sum": sum(numbers),
...: "mean": sum(numbers) / len(numbers),
...: }
In [3]: description
Out[3]: {'length': 8, 'sum': 35, 'mean': 4.375}
请注意,数字列表的总和(sum)和长度(len)都要计算两次。在这个简单的示例中,后果并不严重,但如果列表更大或计算更复杂,可能需要优化代码。为此,可以先将函数调用移出字典定义:
In [4]: numbers = [2, 8, 0, 1, 1, 9, 7, 7]
In [5]: num_length = len(numbers)
In [6]: num_sum = sum(numbers)
In [7]: description = {
...: "length": num_length,
...: "sum": num_sum,
...: "mean": num_sum / num_length,
...: }
In [8]: description
Out[8]: {'length': 8, 'sum': 35, 'mean': 4.375}
变量 num_length
和 num_sum
仅用于优化字典内部的计算。通过使用海象运算符,可以使这一作用更加明确:
In [9]: numbers = [2, 8, 0, 1, 1, 9, 7, 7]
In [10]: description = {
...: "length": (num_length := len(numbers)),
...: "sum": (num_sum := sum(numbers)),
...: "mean": num_sum / num_length,
...: }
In [11]: description
Out[11]: {'length': 8, 'sum': 35, 'mean': 4.375}
num_length
和 num_sum
现在定义在 description 字典的定义中。这清楚地提示了任何阅读这段代码的人,这些变量只是用来优化这些计算,以后不会再使用。
【注意】:在使用海象运算符的示例中,
num_length
和num_sum
变量的作用域与不使用海象运算符的示例中相同。这意味着在这两个示例中,变量都可以在定义描述后使用。
尽管两个示例在功能上非常相似,但使用:=
操作符传达了这些变量作为一次性优化变量的意图。
示例二:
# 传统方式
results = []
for line in lines:
stripped_line = line.strip()
if stripped_line:
results.append(other_fun(stripped_line))
# 海象操作符
results = [other_fun(stripped_line) for line in lines if (stripped_line := line.strip())]
在传统方式中,必须先去掉前后空格,然后检查其是否为空,才能将其添加到结果列表中。然而,使用海象运算符后,该行将作为列表的一部分,去掉前后空格并检查是否为空,从而无需额外的 if 语句。这使得代码更加简洁易读,同时也减少了重复变量赋值的需要。
示例三:将使用 wc.py 来计算文本文件中的行数、字数和字符数:
# wc.py
import pathlib
import sys
# sys.argv 是一个包含命令行参数的列表
for filename in sys.argv[1:]:
# 将每个文件名字符串转换为 pathlib.Path 对象。将文件名存储在 Path 对象中,可以方便地读取下一行的文本文件。
path = pathlib.Path(filename)
# 构造一个计数元组,表示一个文本文件中的行数、字数和字符数。
counts = (
# 读取文本文件,通过计算换行来计算行数
path.read_text().count("\n"),
# 读取文本文件,通过分割空白来计算字数
len(path.read_text().split()),
# 读取文本文件,通过计算字符串的长度来计算字符数
len(path.read_text()),
)
# 将所有三个计数和文件名一起打印到控制台,`*counts` 语法会解包计数元组,等同于 print(counts[0],counts[1],counts[2],path)
print(*counts, path)
该脚本可以读取一个或多个文本文件,并报告每个文件包含多少行、字和字符。下面是代码的详细说明:
运行wc.py检查自身,如下所示:
$ python wc.py wc.py
13 34 316 wc.py
换句话说,wc.py 文件共有 13 行、34 个单词和 316 个字符。
分析代码,会发现并非最佳实现,尤其对 path.read_text()
的调用要重复三次。这意味着每个文本文件都要读取三次。这里就可以使用海象运算符来避免重复:
# wc.py
import pathlib
import sys
for filename in sys.argv[1:]:
path = pathlib.Path(filename)
counts = [
(text := path.read_text()).count("\n"), # Number of lines
len(text.split()), # Number of words
len(text), # Number of characters
]
print(*counts, path)
文件内容被分配给text变量,并在接下来的两次计算中重复使用。程序的功能仍然相同:
$ python wc.py wc.py
13 36 302 wc.py
当然除海象运算符外,另一种常规方法是在定义计数之前先定义文本:
# wc.py
import pathlib
import sys
for filename in sys.argv[1:]:
path = pathlib.Path(filename)
text = path.read_text()
counts = [
text.count("\n"),
len(text.split()),
len(text),
]
print(*counts, path)
虽然该方式在代码量上多于采用海象运算符的方式,但它可能在可读性和效率之间取得了最佳平衡。因此即使 :=
操作符能使代码更简洁,但它并不总是最易读的解决方案。
示例四:列表推导式
列表推导式对于构建和过滤列表非常有用。它们清楚地表达了代码的意图,通常运行速度相当快。
在下面列表推导式用例中,海象运算符特别有用。比方说,想对列表中的元素应用某个计算“昂贵”的函数 slow(),并对结果值进行过滤。可以这样做
numbers = [7, 6, 1, 4, 1, 8, 0, 6]
results = [slow(num) for num in numbers if slow(num) > 0]
此例中,不仅需要过滤数字列表,并保留应用 slow() 后的结果。这段代码的问题在于,这个“昂贵”的函数被调用了两次。
应对这种情况,一种非常常见的解决方案是重写代码,使用显式 for 循环:
results = []
for num in numbers:
value = slow(num)
if value > 0:
results.append(value)
这样只会调用 slow() 函数一次。遗憾的是,现在的代码更加冗长,代码的意图也更难理解。列表推导式清楚地表明我们正在创建一个新列表,而在显式 for 循环中,这一点更加隐蔽,因为创建列表和使用 .append()
之间隔着好几行代码。另外,列表推导式比重复调用 .append()
运行得更快。
当然我们可以使用 filter()
表达式或双列表推导式来编写其他解决方案:
# filter()
results = filter(lambda value: value > 0, (slow(num) for num in numbers))
# 双列表推导式
results = [value for num in numbers for value in [slow(num)] if value > 0]
上述两种方式都只需调用一次 slow(),但这两种表达式都降低了代码的可读性。
现在我们可以使用海象运算符重写列表推导式,如下所示:
results = [value for num in numbers if (value := slow(num)) > 0]
【注意】,value := slow(num)
括号是必需的。
可以明显看到采用海象运算符后代码有效、可读性强,并很好地传达了代码的意图。
该示例中,重点举例说明如何使用海象运算符重写列表推导式。同样的原则也适用于字典推导式、集合推导式或生成器表达式中重复操作的情况。
Python 有两种不同的循环结构:for 循环和 while 循环。当需要遍历已知的元素序列时,通常会使用 for 循环。而 while 循环则用于事先不知道需要循环多少次的情况。
看个简单例子:
# 常规方式1:
data = get_data()
while data:
other_fun(data)
data = get_data()
# 常规方式2:
while True:
data = get_data()
if data is None:
break
other_fun(data)
# 海象运算符
while (data := get_data()) is not None:
other_fun(data)
使用常规的 while 循环条件方法,必须使用重复或者冗余代码来验证数据是否为None。然而,通过使用海象运算符,代码变得更加简洁和优雅。循环条件的验证是赋值的一部分,因此无需额外的 if 语句。
在 Python 中,赋值操作符 (=) 和相等比较操作符 (==) 在视觉上的相似性可能会导致 bug。在引入海象运算符时,为了避免类似bug,一个重要的特点是 :=
运算符绝不允许直接替换 =
运算符,反之亦然。
正如本文开头所述,海象运算符不能使用普通赋值表达式赋值:
>>> walrus := True
File "", line 1
walrus := True
^
SyntaxError: invalid syntax
当然,使用赋值表达式只赋值在语法上是合法的,但同时必须加上括号:
>>> (walrus := True)
True
虽然支持这样做,但这不是好的处理方式,在这里使用普通赋值表达式赋值更优,代码语意更清楚。
PEP 572 还显示了其他几个例子,在这些例子中,:=
操作符要么是非法的,要么是不鼓励使用的。以下示例都会引发语法错误:
>>> lat = lon := 0
SyntaxError: invalid syntax
>>> angle(phi = lat := 59.9)
SyntaxError: invalid syntax
>>> def distance(phi = lat := 0, lam = lon := 0):
SyntaxError: invalid syntax
这些情况下,最好使用 =
代替。接下来的示例与此类似,都是合法代码。但是,在这些情况下,海象运算符并不能改善你的代码:
>>> lat = (lon := 0)
>>> angle(phi = (lat := 59.9))
>>> def distance(phi = (lat := 0), lam = (lon := 0)):
... pass
...
上面示例都无法提高代码的可读性。应该使用传统的赋值语句赋值。请参阅 PEP 572 。
其他情况:
在 f-strings 中,冒号 :
用于分隔数值和格式规范。例如
>>> x = 3
>>> f"{x:=8}"
' 3'
在这种情况下,:=
看起来确实像海象运算符,但效果却截然不同。为了解释 f-strings 中的 x:=8
,表达式被分成三个部分:x
,:
,和 =8
。
这里 x
是值,:
是分隔符,=8
是格式规范。根据 Python 的格式规范,在这种情况下,=
表示对齐选项。上例值会在宽度为 8 的字段中填充空格。
要在 f-strings 内使用海象运算符,需要添加括号:
>>> x = 3
>>> f"{(x := 8)}"
'8'
>>> x
8
不过最好还是在f-strings内使用普通赋值表达式。
让我们看看赋值表达式不合法的其他一些情况:
只能为简单名称赋值,不能为带点名称或索引名称赋值:
>>> (mapping["hearts"] := "love")
SyntaxError: cannot use assignment expressions with subscript
>>> (number.answer := 42)
SyntaxError: cannot use assignment expressions with attribute
使用海象运算符时不能解包:
>>> lat, lon := 59.9, 10.8
SyntaxError: invalid syntax
如果在整个表达式最外层加上括号(lat, lon := 59.9, 10.8)
,将被解释为包含三个元素 "lat、59.9 和 10.8"的元组。
不能将海象运算符与增强赋值运算符(如 +=)结合使用,会语法错误:
>>> count +:= 1
SyntaxError: invalid syntax
最简单的解决方法,例如:可以执行 (count := count + 1)。
作用域与传统赋值语句类似;
括号使if语句更清晰:
例如:
>>> number = 3
>>> if square := number ** 2 > 5:
... print(square)
...
True
square 得到的值是 True(number ** 2 > 5
),而不是 number ** 2
的值。这种情况下,可以用括号来限定表达式:
>>> number = 3
>>> if (square := number ** 2) > 5:
... print(square)
...
9
使用海象运算符赋值元组时,必须在元组周围使用括号:
>>> walrus = 3.7, False
>>> walrus
(3.7, False)
>>> (walrus := 3.8, True)
(3.8, True)
>>> walrus
3.8
>>> (walrus := (3.8, True))
(3.8, True)
>>> walrus
(3.8, True)
在 Python 中,海象运算符 (:=
) 是一个很有用的工具,它可以提高代码的简洁性和表达能力。但是,在使用它之前,必须考虑项目要求和目标 Python 版本的兼容性限制。通过使用海象运算符,可以创建高效、优雅的代码,但同时需要避免滥用导致代码可读性降低。