跳至主要内容

Python 的海象运算符:赋值表达式(教程)

Python 的海象运算符 := 允许您在表达式中为变量赋值,将赋值与使用合二为一。
· 8分钟 阅读

使用 AI 探索

Python 3.8 为该语言引入了一个新的运算符。其官方名称为赋值表达式,使用 := 运算符。

不过,人们更常称其为海象运算符,因为它看起来像海象的獠牙!

当该运算符被添加到语言中时,一向平静的 Python 世界也出现了争议,而且它的使用仍相对有限。尽管如此,赋值表达式如今已成为 Python 的一部分,并且在某些场景中很有用。

本教程将探讨 Python 的海象运算符如何工作,并讨论其适用场景。

什么是 Python 的海象运算符

使用 := 运算符的 赋值表达式看起来与更常见的 赋值语句类似,后者使用 = 运算符。不过,两者之间有一个重要区别。

一个 语句是执行某些操作的代码单元。 表达式是能被求值为一个值的语句。因此,表达式会返回一个值,而非表达式的语句不会返回任何值。

Python 的 REPL(读取、求值、打印、循环)会在表达式被求值后立即显示其值。但是,当执行一个不是表达式的语句时,不会显示任何值:

>>> import random  # Not an expression
>>> 10 * 2  # An expression
20

import 语句不会求值为任何值。而 10 * 2 是一个会被求值为某个值的表达式。

使用 = 运算符的赋值语句会将对象赋给变量或其他引用,但不会返回该值。赋值表达式(海象运算符)执行与赋值语句相同的操作,但还会返回该对象:

>>> value = 10  # Assignment statement
>>> value
10
>>> (value := 20)  # Assignment expression
20

请注意,与赋值表达式不同,赋值语句不会返回值。因此,一个赋值表达式组合了多个操作:

  1. 对 := 运算符右侧的表达式求值。
  2. 将该值赋给一个变量名。
  3. 返回该值。

在用赋值表达式替代赋值语句时,如本例所示,语法上必须使用括号以减少歧义。关于海象运算符的语法要求,稍后在本教程中会进一步介绍。

在 Python 中,赋值表达式可以出现在任何允许使用表达式的地方。来看一个例子:

print(f"The number is {(value := 50)}")
print(value)
The number is 50
50

变量名 value 在第一行的 f 字符串中被定义。同时,该整数也直接返回给 f 字符串中的花括号。f 字符串中的花括号必须包含一个表达式,这也解释了为何不能使用赋值语句。

Python 海象运算符的使用场景

海象运算符的两个常见用途是简化和优化代码。

简化代码

海象运算符的一个常见用法是在需要先定义变量再使用的地方(例如某些条件语句)简化代码。考虑下面的 while 循环:

import random
value = random.randint(1, 20)
while value < 18:
    print(value)
    value = random.randint(1, 20)
7
5
1
11
12
1
9

这段代码在生成的随机值小于 18 时运行 while 代码块。变量 value 需要在 while 循环之前定义,以便在 while 语句中使用。在 while 代码块的末尾,会将一个新的随机数赋给 value。

可以使用赋值表达式 := 来减少代码行数,并在程序中的同一处调用 random.randint() 函数:

import random
while (value := random.randint(1, 20)) < 18:
    print(value)
1
9
2
4
9
15
11

每次 while 循环开始新一轮迭代时,都会生成一个新的随机数并将其赋给 value。

优化代码

我们再来看一个使用列表推导式的例子。考虑以下代码,它过滤包含日期的字符串列表,并创建一个仅包含指定年份日期的 datetime.datetime 对象的列表:

from datetime import datetime
def format_date(date_str):
    return datetime.strptime(date_str, "%Y-%m-%d")
dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"]
formatted_dates = [
    format_date(date)
    for date in dates
    if format_date(date).year == 2024
]
print(formatted_dates)
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]

函数 format_date() 接受一个采用yyyy-mm-dd 格式的日期字符串,并返回一个 datetime.datetime 对象。datetime.datetime 对象的属性之一是 .year,表示日期的年份部分。

最终列表中只包含 2024 年的三条日期,并且列表中是 datetime.datetime 对象而非字符串。该列表推导式包含一个调用 format_date() 的 if 子句。然而,format_date() 也在列表推导式的第一个表达式中被调用,用于生成要添加到列表中的值。用相同参数调用该函数两次效率不高,尤其当该函数是程序性能的瓶颈时。

一种避免重复调用相同函数的方法是先调用函数并将其赋给变量,然后再使用它。不过,在仍使用列表推导式的情况下,无法通过赋值语句实现。这种方案需要一个标准的 for 循环:

from datetime import datetime
def format_date(date_str):
    return datetime.strptime(date_str, "%Y-%m-%d")
dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"]
formatted_dates = []
for date in dates:
    formatted_date = format_date(date)
    if formatted_date.year == 2024:
        formatted_dates.append(formatted_date)
print(formatted_dates)
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]

海象运算符提供了与列表推导式兼容的另一种选择,因为函数返回的格式化日期可以在同一表达式中既赋给变量名又返回:

from datetime import datetime
def format_date(date_str):
    return datetime.strptime(date_str, "%Y-%m-%d")
dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"]
formatted_dates = [
    formatted_date
    for date in dates
    if (formatted_date := format_date(date)).year == 2024
]
print(formatted_dates)
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]

列表推导式中的赋值表达式是括号内的表达式 (formatted_date := format_date(date))。该表达式调用函数 format_date(),将其值赋给变量 formatted_date,并返回该值。由于赋值表达式会被求值为一个 datetime.datetime 对象,代码便访问其 .year 属性,并使用相等运算符将其与 2024 比较。

列表推导式中的 if 子句会在生成要存入列表值的表达式之前执行。因此,海象运算符放在 if 子句中使用,而在该赋值表达式中定义的变量则作为列表推导式的第一个表达式被使用。

围绕 Python 海象运算符的争议

并非所有 Python 程序员都喜欢赋值表达式。有人认为它能简化代码,但也有人觉得它让代码更难读,宁愿选择更冗长的写法。可读性具有主观性,因此对于海象运算符是否让代码更易读,意见不一。

上一节的最后一个示例展示了所需的权衡。如果不使用海象运算符,该示例中的代码需要先初始化一个空列表,然后在 for 循环中向其追加值。与为此类工作而优化的列表推导式相比,这种方案更长且效率更低。

然而,对于一些更愿意写更多代码、而非使用海象运算符的程序员来说,如果性能不是问题,标准的 for 循环方案可能更具可读性。

许多编码风格指南仍建议避免使用赋值表达式,或仅在必要时少量使用。

Python 海象运算符:语法规则

在最初尝试海象运算符时,常会因其语法规则而遇到语法错误。

为避免将赋值语句 = 与赋值表达式 := 混淆,不存在两者在同一位置都语法有效的情形。因此,通常需要用括号包裹赋值表达式:

(value := 20)  # Valid
value := 20  # Invalid (SyntaxError)

在这种情况下,必须将赋值表达式置于括号中,这与赋值语句的要求正好相反——赋值语句不能放在括号里。

不过,赋值表达式并非总是需要括号:

import random
if value := random.randint(0, 3):
    print(f"{value} is greater than 0")
1 is greater than 0

if 语句中的赋值表达式会生成一个 0 到 3 之间的随机数,并将其赋给变量 value。整数 1、2 和 3 被视为真值(truthy),当生成这些值时,if 代码块会运行。而当 random.randint() 返回被视为假值的 0 时,if 代码块不会执行。

赋值语句在 if 关键字之后是无效的。因此,在这种情况下不存在混淆,也无需括号。

该示例还演示了关于赋值表达式的另一个关键点。被赋值变量的作用域遵循通常的 LEGB 规则。因此,在此示例中,变量 value 在全局作用域内的任意位置都可用:

import random
if value := random.randint(0, 3):
    print(f"{value} is greater than 0")
   
print(value)
2 is greater than 0
2

如果在函数定义内使用赋值表达式,则该变量仅在函数内的局部范围可用。这与使用赋值语句创建的变量行为相同。

可以修改此示例来展示使用海象运算符的另一个潜在陷阱。让我们将这段代码改为在 if 语句中包含一个比较运算符:

import random
if value := random.randint(0, 3) < 2:
    print(f"{value} is less than 2")
True is less than 2

输出提示了这段代码的问题,因为 value 是布尔值 True 而非整数。海象运算符在所有 运算符优先级中优先级最低,仅高于逗号。因此,小于运算符 < 会先被求值,其输出被赋给 value。此时需要使用括号来处理:

import random
if (value := random.randint(0, 3)) < 2:
    print(f"{value} is less than 2")
0 is less than 2

由于括号具有最高优先级,如今赋值表达式会先被求值。random.randint() 返回的数字被赋给 value,并作为小于运算的第一个操作数。

与赋值语句不同,海象运算符只能将值赋给变量名,不能用于给下标或属性赋值:

(name := "James")  # Valid
points = {"James": 10, "Mary": 20}
(points["Sarah"] := 30)  # Invalid
class Article:
    pass
article = Article()
(article.title := "The Walrus Operator")  # Invalid

无法使用海象运算符通过下标或向属性赋值。

结语

使用 := 语法的赋值表达式(通常称为海象运算符)是 Python 的一个相对较新的特性。使用 = 运算符的赋值语句仍是为变量名赋值的更常见方式。

不过,海象运算符扩展了赋值操作,同时返回被赋的值,从而使该赋值本身成为表达式。因此,您可以在 Python 允许使用其他表达式的任何地方使用赋值表达式。

然而,Python 社区对海象运算符的实用性和可读性看法不一。出于这个原因,赋值表达式的使用频率不如 Python 的其他新特性那样高。掌握其用法当然很好,但也请勿滥用!

Stephen Gruppetta

作者

Stephen Gruppetta

我在马耳他大学完成了本科阶段的物理与数学学习。随后前往伦敦,并在帝国理工学院取得物理学博士学位,研究用于成像人类视网膜的新型光学技术。现在,我专注于撰写与传播 Python 相关内容,并教授 Python。

主题