PEP572:赋值表达式(海象符)(海象运算符有什么作用)
off999 2024-11-17 14:32 54 浏览 0 评论
阅读 PEP 是理解 Python 特性的绝好方式。Python 3.8 引入了赋值表达式,它是什么?怎么用?有什么限制?话不多说,直接看 PEP。
一、简介
本提案建议在 Python 中增加 := 运算符,使我们可以在表达式中直接赋值给变量。
增加这个运算符后,字典推导式的计算顺序也将作出调整,从而确保 键 的计算先于 值 的计算(因为 键 的值可能会被绑定在一个变量名称上,用于 值 的计算)。
在本提案的讨论过程中,:= 被非正式地称为“海象符”("the walrus operator")。带这种运算符的表达式,正式名称是“赋值表达式”("Assignment Expressions",即本提议的标题),有时也被称为“命名表达式”("Named Expressions",例如 CPython 实现中即以此作为内部名称)。
二、必要性说明
命名某个表达式的结果是编程中的重要一环,使我们只需记住一个简单的名称,而不是一长串的表达式,并且也容易复用。目前,Python 只能在赋值声明中进行命名,因而在列表推导式或一些其它场景下,就无法进行命名。
另外,在交互式 debug 过程中,命名某个大型表达式的一部分可以帮助我们做一些深入的检查。如果无法获取表达式的局部结果,就往往需要在调试过程中重构代码;通过赋值表达式,这些重构将被几个简单的 := 替代。
由于不再需要重构代码,我们在调试过程中不经意地改变代码逻辑的几率也降低了(调试过程中的重构,是导致 海森堡Bug <Heisenbugs> 的常见原因),同时让我们更容易向别的程序员解释程序逻辑。
(译注:所谓 Heisenbugs,就是当我们调试的时候,这个 bug 会莫名其妙地消失,命名取自 维尔纳·海森堡 提出的量子力学观察者效应:观察系统的行为将不可避免地将改变其状态。)
2.1 使用真实代码进行讨论的重要性
在本提案的讨论过程中,许多人(不管是支持者还是反对者)都有一种使用过度简化,或者过度复杂的例子的倾向。
使用过度简化的例子时,往往让人感觉是在吹毛求疵,或者可以直接反驳“我反正是绝不会写出这样的代码来的”。而使用过度复杂的例子时,也容易让人感觉含混不清。
当然,这两种例子依然是有意义的:它们可以帮助我们澄清一些语义学的上的概念。因此,我们还是会用到一些这样的示例。
不论如何,讨论中使用的例子,最好还是来自真实的代码。也就是说,来自大大小小的真实应用,并且在写这些代码时,还没有考虑到本提案的存在。
Tim Peters 检查了他自己的代码库,找出许多(在他看来)可以通过赋值表达式写得更清楚的案例,他的最终结论是:本提案确实可以,虽然在比较小的程度上,改进不少代码。
使用真实代码的另一个好处是,我们可以间接地观察程序员们对紧凑的理解。Guido van Rossum 检查了 Dropbox 的代码库,发现程序员们更倾向于少写一些代码行,而不是缩短每行代码的长度。
比方说,Guido 发现,有些程序员宁肯重复地写几个短表达式,导致程序变慢,也不愿多写一行代码。例如,与其写这样的代码:
match = re.match(data)
group = match.group(1) if match else None程序员更喜欢这样写:
group = re.match(data).group(1) if re.match(data) else None另一种情况是,程序员有时宁肯多跑一些代码,也不愿多写一层缩进:
match1 = pattern1.match(data)
match2 = pattern2.match(data)
if match1:
result = match1.group(1)
elif match2:
result = match2.group(2)
else:
result = None在上面的代码中,match2 在 match1 已经 match 的时候依然会 match,实际上是没有必要的,更高效的写法应该是:
match1 = pattern1.match(data)
if match1:
result = match1.group(1)
else:
match2 = pattern2.match(data)
if match2:
result = match2.group(2)
else:
result = None三、句法与语义
在可以使用 Python 表达式的大多数地方,都可以使用命名表达式。具体形式为 NAME := expr ,expr 是一个有效的 Python 表达式,NAME 是一个标识符。
命名表达式的值与对应表达式是一样的,只是可以同时赋值给某个变量:
# 正则匹配
if (match := pattern.search(data)) is not None:
# Do something with match
# 迭代器循环
while chunk := file.read(8192):
process(chunk)
# 重用一个计算复杂的变量
[y := f(x), y**2, y**3]
# 重用推导式过滤器中的计算结果
filtered_data = [y for x in data if (y := f(x)) is not None]3.1 例外情况
赋值表达式不能用于一些特定场景,主要是为了避免语义混淆:
- 不能用于直接的赋值声明,除非用括号括起来。例如:
y := f(x) # 错误
(y := f(x)) # 正确,但不推荐这个设定主要是帮助大家区别 赋值声明 与 赋值表达式 ——任何情况下,它们中最多只有一个符合语法规范。
- 不能用于直接的赋值声明的右侧,除非用括号括起来。例如:
y0 = y1 := f(x) # 错误
y0 = (y1 := f(x)) # 正确,但不鼓励理由同上。
- 不能用于调用函数时的关键字参数,除非用括号括起来。例如:
foo(x = y := f(x)) # 错误
foo(x=(y := f(x))) # 正确,但很奇怪这个设定主要是为了避免一些容易引起混淆的代码,并且获取函数参数的过程本身已经很复杂了。
- 不能用于函数参数的默认值,除非用括号括起来。例如:
def foo(answer = p := 42): # 错误
...
def foo(answer=(p := 42)): # 正确,但有点丑陋
...函数参数的具体语法对很多用户来说已经很难理解了(例如,可变对象作为参数默认值等),因此,避免赋值表达式再来添乱,并且也与前一个设定相呼应。
- 不能用于函数参数的类型注解,除非用括号括起来:
def foo(answer: p := 42 = 5): # 错误
...
def foo(answer: (p := 42) = 5): # 正确,但可能没人会这么写
...理由与前面两点的理由相似,各种各样的 : 和 = 堆在一起,影响代码可读性。
- 不能用于匿名函数,除非用括号括起来。例如:
(lambda: x := 1) # 错误
lambda: (x := 1) # 正确,但好像没什么用
(x := lambda: 1) # 正确
lambda line: (m := re.match(pattern, line)) and m.group(1) # 正确在匿名函数的最外层命名一个变量没有意义,因为无法使用这个变量。为了复用这个变量,总是要加一个括号的,因此,这个设定应该不会影响到大家的代码。
- 在 f-strings 格式化中使用赋值表达式时,必须使用括号。例如:
>>> f'{(x:=10)}' # 正确,使用了赋值表达式
'10'
>>> x = 10
>>> f'{x:=10}' # 正确,正常使用格式化定义,将 '=10' 作为格式化参数
' 10'这也意味着,在 f-string 中,带 := 不一定就是赋值表达式。f-string 使用 : 传递格式化参数,为了向后兼容,这里的赋值表达式必须使用括号括起来。当然,这种用法并不推荐。
3.2 作用域
赋值表达式并不会引入新的作用域。大多数情况下,它所在的作用域是很明确的:就是当前作用域,如果这个作用域中使用了 nolocal 或 global 变量,赋值表达式也可以使用。而一个匿名函数(虽然是匿名的,但也是一个函数)本身也会引入一个作用域。
但有一种特殊情况,列表、集合、字典推导式与生成器表达式(一下统一称为推导式)中的赋值表达式,作用域为这些推导式所在的作用域,并且可以使用原作用域中的 nolocal 或 global 变量。为了更好地支持这一规则,递归推导式中的赋值表达式,作用域在最外层推导式所在的作用域。当然,如果最外层推导式是在一个匿名函数中的话,赋值表达式的作用域就是这个匿名函数自身的作用域。
这样设计有两个目的,一是使我们能方便地调用 any() 或 all() 函数,例如:
if any((comment := line).startswith('#') for line in lines):
print("First comment:", comment)
else:
print("There are no comments")
if all((nonblank := line).strip() == '' for line in lines):
print("All lines are blank")
else:
print("First non-blank line:", nonblank)二是使我们能很容易地计算推导式中的累计状态,例如:
# 计算列表推导式中的累计和
total = 0
partial_sums = [total := total + v for v in values]
print("Total:", total)当然,赋值表达式中的标识符名称不能与推导式所用的变量名称相同。因为推导式本身所用的变量,作用域只在推导式中,而命名表达式中的标识符,作用域在最外层推导式所在的作用域中,两者相同必然会产生冲突。
例如,[i := i+1 for i in range(5)] 是错误的,推导过程中所用的变量名 i 作用域在推导式中,而 i := 部分的 i 的作用域并不局限于这个推导式。同样,以下这些示例也都是错误的:
[[(j := j) for i in range(5)] for j in range(5)] # 错误
[i := 0 for i, j in stuff] # 错误
[i+1 for i in (i := stuff)] # 错误就以上示例来说,技术上,我们也可以为它们设计一个统一的语法规则,但很难说这种规则在实践中有什么用处。因此,内核实现中,遇到这些场景,会直接抛出 SyntaxError。
这个限制即使在赋值表达式并不会被执行时也是生效的:
[False and (i := 0) for i, j in stuff] # 错误
[i for i, j in stuff if True or (j := 1)] # 错误对于推导式中的推导部分(第一个 for 之前的部分)或过滤器部分( if 之后,任意嵌套的 for 之前的部分),不能重名的限制只针对推导式中的迭代变量。如果在这些地方有匿名函数,则由于匿名函数引入了新的作用域,因此依然可以无限制地使用赋值表达式。
由于内核实现上的设计限制(符号表分析器 symbol table analyser 很难判断推导式最左侧的迭代部分是否与其它部分重用名称 ),推导式的迭代部分完全禁用命名表达式( in 之后,并在可能的 if 或 for 之前的部分):
[i+1 for i in (j := stuff)] # 错误
[i+1 for i in range(2) for j in (k := stuff)] # 错误
[i+1 for i in [j for j in (k := stuff)]] # 错误
[i+1 for i in (lambda: (j := stuff))()] # 错误另外一个特例就是,如果推导式在一个类作用域中,并且其中的赋值表达式的赋值结果也在这个类作用域中,也会抛出 SyntaxError:
class Example:
[(j := i) for i in range(5)] # 错误(这个特例是由推导式所创建的隐式函数作用域导致的——目前还没有让函数直接调用该函数所在的类作用域中的变量的运行时机制,并且我们也无意于增加这种机制。如果之后这个问题解决了,针对赋值表达式的这个限制也可能会取消。请注意,在推导式中无法使用其所在的类作用域中所定义的变量,是一个已经存在的问题。)
(译注:这个问题有历史原因,与生成器表达式的设计有关,想要理解具体是什么问题可以参考 stackover上的回答,想要理解这样设计的原因,可以参考 PEP289,之后有机会的话,也会翻译推荐给大家。)
参考附录 B ,可以看到一些将推导式转换为等效代码,从而绕过命名冲突的例子。
3.3 := 运算符的优先级
:= 的优先级高于逗号,低于其它所有操作符,包括 or,and,以及条件表达式(A if C else B)。如前文所说,:= 永远不会与 = 比较优先级(除非通过括号分隔开了)。
:= 可直接用于函数的位置参数,但不能用于关键字参数。
以下例子或许有助于我们理解这些规则:
# 错误
x := 0
# 替代写法
(x := 0)
# 错误
x = y := 0
# 替代写法
x = (y := 0)
# 正确
len(lines := f.readlines())
# 正确
foo(x := 3, cat='vector')
# 错误
foo(cat=category := 'vector')
# 替代写法
foo(cat=(category := 'vector'))以上大多数所谓“正确”的写法都是不推荐的写法,因为阅读代码的人往往一扫而过,可能容易看混。但在一些简单场景中还是可以使用的:
# 正确
if any(len(longline := line) >= 100 for line in lines):
print("Extremely long line:", longline)本提案推荐大家在 := 两侧分别留一个空格,正如 PEP8 对 = 作为赋值符号时的建议一样。当然,在指定关键字参数时,= 的两侧不用留空格 : )
3.4 计算顺序的调整
为确保语法定义精确,计算顺序也需要被精确定义。技术上说,计算顺序不是一个新问题,因为函数调用过程可能本身就要有一些控制。Python 已有的规则是,子表达式会逐步从左往右计算。赋值表达式使我们在函数调用过程中进行控制的需要更明确了,因此,我们对当前计算顺序做了一个调整:
在字典推导式 {X: Y for ...} 中,按原来的规则,Y 是先于 X 计算的,我们建议让 X 的计算先于 Y。(其实,在形如 {X: Y} 或 dict((X, Y) for ...) 的字典创建过程中,X 的计算就是先于 Y 的,我们只是把同样的规则也推广到字典推导式中。)
3.5 赋值表达式与赋值声明的区别
最重要的区别是,:= 是一个表达式,因此可以被用于很多赋值声明不能使用的场景,包括匿名函数与推导式。
反过来说,赋值表达式也不能支持一些赋值声明的特性:
- 不直接支持多个对象赋值:
x = y = z = 0 # 等效代码: (z := (y := (x := 0)))- 不支持非名称的赋值对象:
# 无对应的等效代码
a[i] = x
self.rest = []- 对逗号的运算优先级不同:
x = 1, 2 # x 为 (1, 2)
(x := 1, 2) # x 为 1- 不支持迭代器拆包(包括常规形式与扩展形式):
# 等效代码需要加括号
loc = x, y # 等效代码 (loc := (x, y))
info = name, phone, *rest # 等效代码 (info := (name, phone, *rest))
# 无等效代码
px, py, pz = position
name, phone, email, *other_info = contact- 不支持行内类型注释:Inline type annotations are not supported:
# 最接近的等效代码是单独声明 "p: Optional[int]" 然后赋值
p: Optional[int] = None- 不支持增量赋值:
total += tax # 等效代码 (total := total + tax)四、使用示例
4.1 标准库中的使用示例
site.py
env_base 只在这个判断语句中使用,因此直接放到 if 之后:
- 原代码:
env_base = os.environ.get("PYTHONUSERBASE", None)
if env_base:
return env_base- 改进后:
if env_base := os.environ.get("PYTHONUSERBASE", None):
return env_base_pydecimal.py
取消 if 语句的嵌套,减少一层缩进:
- 原代码:
if self._is_special:
ans = self._check_nans(context=context)
if ans:
return ans- 改进后:
if self._is_special and (ans := self._check_nans(context=context)):
return anscopy.py
避免 if 语句的多层嵌套。(本例还可以参考附录 A )
- 原代码:
reductor = dispatch_table.get(cls)
if reductor:
rv = reductor(x)
else:
reductor = getattr(x, "__reduce_ex__", None)
if reductor:
rv = reductor(4)
else:
reductor = getattr(x, "__reduce__", None)
if reductor:
rv = reductor()
else:
raise Error(
"un(deep)copyable object of type %s" % cls)- 改进后:
if reductor := dispatch_table.get(cls):
rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
rv = reductor(4)
elif reductor := getattr(x, "__reduce__", None):
rv = reductor()
else:
raise Error("un(deep)copyable object of type %s" % cls)datetime.py
tz 只在 s += tz 中使用,把赋值放到 if 语句中使作用域更明确。
- 原代码:
s = _format_time(self._hour, self._minute,
self._second, self._microsecond,
timespec)
tz = self._tzstr()
if tz:
s += tz
return s- 改进后:
s = _format_time(self._hour, self._minute,
self._second, self._microsecond,
timespec)
if tz := self._tzstr():
s += tz
return ssysconfig.py
在 while 语句调用 fp.readling(),在 if 语句调用 match() ,使代码更紧凑:
- 原代码:
while True:
line = fp.readline()
if not line:
break
m = define_rx.match(line)
if m:
n, v = m.group(1, 2)
try:
v = int(v)
except ValueError:
pass
vars[n] = v
else:
m = undef_rx.match(line)
if m:
vars[m.group(1)] = 0- 改进后:
while line := fp.readline():
if m := define_rx.match(line):
n, v = m.group(1, 2)
try:
v = int(v)
except ValueError:
pass
vars[n] = v
elif m := undef_rx.match(line):
vars[m.group(1)] = 04.2 简化列表推导式
通过获取过滤器计算结果,可以更高效地进行列表推导:
results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0]类似地,可以引入赋值表达式,使子表达式在主表达式中复用:
stuff = [[y := f(x), x/y] for x in range(5)]注意,在以上两个例子中,变量 y 的作用域都是推导式所在的作用域(即与 results 或 stuff 为同一个作用域)。
4.3 获取条件计算结果Capturing condition values
赋值表达式可用于获取 if 或 while 语句中的条件计算结果:
# 循环交互
while (command := input("> ")) != "quit":
print("You entered:", command)
# 获取正则表达式的 match 结果
# 可以查看 Lib/pydoc.py 中的更多示例
if match := re.search(pat, text):
print("Found:", match.group(0))
# 把 match 赋值放在 elif 语句中,避免了多层缩进
elif match := re.search(otherpat, text):
print("Alternate found:", match.group(0))
elif match := re.search(third, text):
print("Fallback found:", match.group(0))
# 读取 socket 数据,直到遇到空字符串:
while data := sock.recv(8192):
print("Received data:", data)在 while 循环中,赋值表达式往往可以避免无限循环的引入。用户可以直接调用函数作为循环条件,并在之后的循环体中使用函数调用的结果。
4.4 Fork
一个来自 UNIX 底层的示例:
if pid := os.fork():
# Parent code
else:
# Child code五、代码风格建议
有些地方可以等效地使用赋值表达式与赋值声明,那么,应该优先使用哪一种呢?我们有以下两条建议:
- 如果可以,优先使用赋值声明,它可以更清楚地表明意图。
- 如果使用赋值表达式可能导致计算顺序不明确,应重构为使用赋值声明的代码。
(译注:本提案还有 3 个附录,本文已经较长,之后再翻译推荐给大家,请多多见谅!)
相关推荐
- 笔记本电脑选哪个品牌比较好
-
1、苹果APPLE/美国2、戴尔DELL/美国3、华为HUAWEI/中国4、小米MI/中国5、微软Microsoft/美国6、联想LENOVO/中国7、惠普HP/美国8、华硕ASUS/...
- 10系列显卡排名(10系显卡性能排行)
-
十系显卡指NVIDIAGeForce10系列,是英伟达研发并推出的图形处理器系列,被用以取代NVIDIAGeForce900系列图形处理器。新系列采用帕斯卡微架构来代替之前的麦克斯韦微架构,并...
-
- 最新win7系统下载(windows7最新版本下载)
-
最简单的方法就是,下载完镜像文件后,直接把镜像文件解压,解压到非C盘,然后在解压文件里面找到setup.exe,点击运行即可。安装系统完成后,在C盘找到一个Windows.old(好几个GB,是旧系统打包在这里,垃圾文件了)删除即可。扩展资...
-
2026-01-15 06:43 off999
- 哪个电脑管家软件好用(哪个电脑管家好用些)
-
腾讯电脑管家吧,因为这个是杀毒和管理合一的,占用内存小,因此显得更为简洁,使电脑运行更加流畅此外电脑诊所,工具箱以及4+1的杀毒模式让腾讯电脑管家也收到了广泛的关注4+1杀毒引擎,管家反病毒引擎、金山...
- 怎么进入win7安全模式(怎么进入win7安全模式界面)
-
方法如下:1、首先进入Win7系统,然后使用Win键+R组合键打开运行框,输入“Msconfig”回车进入系统配置。2、在打开的系统配置中,找到“引导”选项,然后单击,选择Win7的引导项,然后在“安...
- 怎么分区固态硬盘(怎样分区固态硬盘)
-
固态硬盘的分区方法与传统机械硬盘基本相同,以下是一个简单的步骤:1.打开磁盘管理工具:在Windows操作系统中,按下Win+X键,选择"磁盘管理"。或者打开控制面板,在"...
-
- 笔记本声卡驱动怎么下载(笔记本如何下载声卡)
-
1、在浏览器中输入并搜索,然后下载并安装。2、安装完成后打开360驱动大师,它就会自动检测你的电脑需要安装或升级的驱动。3、检测完毕后,我们可以看到我们的声卡驱动需要安装或升级,点击安装或升级,就会开始自动安装或升级声卡了。4、升级过程中会...
-
2026-01-15 05:43 off999
- win10加快开机启动速度(加快开机速度 win10)
-
一、启用快速启动功能1.按win+r键调出“运行”在输入框输入“gpedit.msc”按回车调出“组策略编辑器”?2.在“本地组策略编辑器”依次打开“计算机配置——管理模块——系统——关机”在右侧...
-
- excel的快捷键一览表(excel的快捷键一览表超全)
-
Excel快捷键大全的一些操作如下我在工作中经常使用诸如word或Excel之类的办公软件。我相信每个人都不太熟悉这些办公软件的快捷键。使用快捷键将提高办公效率,并使您的工作更加轻松快捷。。例如,在复制时,请使用CtrI+C进行复制,...
-
2026-01-15 05:03 off999
- 华硕u盘启动按f几(华硕u盘装系统按f几进入)
-
F8。1、开机的同时按F8进入BIOS。2、在Boot菜单中,置secure为disabled。3、BootListOption置为UEFI。4、在1stBootPriority中usb—HD...
- 手机云电脑怎么用(手机云端电脑)
-
使用手机云电脑,您首先需要安装相应的云电脑应用。例如,华为云电脑APP。在安装并打开应用后,您将看到一个显示器的图标,这就是您的云电脑。点击这个图标,您将被连接到一个预装有Windows操作系统和必要...
- ie11浏览器怎么安装(ie11浏览器安装步骤)
-
如果IE浏览器11版本你发现无法正常安装,那么很可能是这样几个原因,一个就是电脑的存储空间不够到时无法安装,再有就是网络的问题,如果没有办法安装的话就不要再安装了,本身这个IE浏览器并不是多好用,你最...
- 台式机重装系统win7(台式机怎么重装win7)
-
下面主要介绍两种方法以重装系统:一、U盘重装系统准备:一台正常开机的电脑和一个U盘1、百度下载“U大师”(老毛桃、大白菜也可以),把这个软件下载并安装在电脑上。2、插上U盘,选择一键制作U盘启动(制作...
- 字母下划线怎么打出来(字母下的下划线怎么去不掉)
-
第一步,在电脑上找到文字处理软件WPS,双击即自动新建一个新文档。第二步,在文档录入需要处理的字母和数字,双击鼠标或拖动鼠标选择要处理的内容。第三步,在页面的左上方的横向菜单栏,找到字母U的按纽,点击...
欢迎 你 发表评论:
- 一周热门
-
-
抖音上好看的小姐姐,Python给你都下载了
-
全网最简单易懂!495页Python漫画教程,高清PDF版免费下载
-
飞牛NAS部署TVGate Docker项目,实现内网一键转发、代理、jx
-
Python 3.14 的 UUIDv6/v7/v8 上新,别再用 uuid4 () 啦!
-
python入门到脱坑 输入与输出—str()函数
-
Python三目运算基础与进阶_python三目运算符判断三个变量
-
(新版)Python 分布式爬虫与 JS 逆向进阶实战吾爱分享
-
失业程序员复习python笔记——条件与循环
-
系统u盘安装(win11系统u盘安装)
-
Python 批量卸载关联包 pip-autoremove
-
- 最近发表
- 标签列表
-
- python计时 (73)
- python安装路径 (56)
- python类型转换 (93)
- python进度条 (67)
- python吧 (67)
- python的for循环 (65)
- python格式化字符串 (61)
- python静态方法 (57)
- python列表切片 (59)
- python面向对象编程 (60)
- python 代码加密 (65)
- python串口编程 (77)
- python封装 (57)
- python写入txt (66)
- python读取文件夹下所有文件 (59)
- python操作mysql数据库 (66)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python多态 (60)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)
