PEP572:赋值表达式(海象符)(海象运算符有什么作用)
off999 2024-11-17 14:32 29 浏览 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 ans
copy.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 s
sysconfig.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)] = 0
4.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 个附录,本文已经较长,之后再翻译推荐给大家,请多多见谅!)
相关推荐
- 面试官:来,讲一下枚举类型在开发时中实际应用场景!
-
一.基本介绍枚举是JDK1.5新增的数据类型,使用枚举我们可以很好的描述一些特定的业务场景,比如一年中的春、夏、秋、冬,还有每周的周一到周天,还有各种颜色,以及可以用它来描述一些状态信息,比如错...
- 一日一技:11个基本Python技巧和窍门
-
1.两个数字的交换.x,y=10,20print(x,y)x,y=y,xprint(x,y)输出:102020102.Python字符串取反a="Ge...
- Python Enum 技巧,让代码更简洁、更安全、更易维护
-
如果你是一名Python开发人员,你很可能使用过enum.Enum来创建可读性和可维护性代码。今天发现一个强大的技巧,可以让Enum的境界更进一层,这个技巧不仅能提高可读性,还能以最小的代价增...
- Python元组编程指导教程(python元组的概念)
-
1.元组基础概念1.1什么是元组元组(Tuple)是Python中一种不可变的序列类型,用于存储多个有序的元素。元组与列表(list)类似,但元组一旦创建就不能修改(不可变),这使得元组在某些场景...
- 你可能不知道的实用 Python 功能(python有哪些用)
-
1.超越文件处理的内容管理器大多数开发人员都熟悉使用with语句进行文件操作:withopen('file.txt','r')asfile:co...
- Python 2至3.13新特性总结(python 3.10新特性)
-
以下是Python2到Python3.13的主要新特性总结,按版本分类整理:Python2到Python3的重大变化Python3是一个不向后兼容的版本,主要改进包括:pri...
- Python中for循环访问索引值的方法
-
技术背景在Python编程中,我们经常需要在循环中访问元素的索引值。例如,在处理列表、元组等可迭代对象时,除了要获取元素本身,还需要知道元素的位置。Python提供了多种方式来实现这一需求,下面将详细...
- Python enumerate核心应用解析:索引遍历的高效实践方案
-
喜欢的条友记得关注、点赞、转发、收藏,你们的支持就是我最大的动力源泉。根据GitHub代码分析统计,使用enumerate替代range(len())写法可减少38%的索引错误概率。本文通过12个生产...
- Python入门到脱坑经典案例—列表去重
-
列表去重是Python编程中常见的操作,下面我将介绍多种实现列表去重的方法,从基础到进阶,帮助初学者全面掌握这一技能。方法一:使用集合(set)去重(最简单)pythondefremove_dupl...
- Python枚举类工程实践:常量管理的标准化解决方案
-
本文通过7个生产案例,系统解析枚举类在工程实践中的应用,覆盖状态管理、配置选项、错误代码等场景,适用于Web服务开发、自动化测试及系统集成领域。一、基础概念与语法演进1.1传统常量与枚举类对比#传...
- 让Python枚举更强大!教你玩转Enum扩展
-
为什么你需要关注Enum?在日常开发中,你是否经常遇到这样的代码?ifstatus==1:print("开始处理")elifstatus==2:pri...
- Python枚举(Enum)技巧,你值得了解
-
枚举(Enum)提供了更清晰、结构化的方式来定义常量。通过为枚举添加行为、自动分配值和存储额外数据,可以提升代码的可读性、可维护性,并与数据库结合使用时,使用字符串代替数字能简化调试和查询。Pytho...
- 78行Python代码帮你复现微信撤回消息!
-
来源:悟空智能科技本文约700字,建议阅读5分钟。本文基于python的微信开源库itchat,教你如何收集私聊撤回的信息。[导读]Python曾经对我说:"时日不多,赶紧用Python"。于是看...
- 登录人人都是产品经理即可获得以下权益
-
文章介绍如何利用Cursor自动开发Playwright网页自动化脚本,实现从选题、写文、生图的全流程自动化,并将其打包成API供工作流调用,提高工作效率。虽然我前面文章介绍了很多AI工作流,但它们...
- Python常用小知识-第二弹(python常用方法总结)
-
一、Python中使用JsonPath提取字典中的值JsonPath是解析Json字符串用的,如果有一个多层嵌套的复杂字典,想要根据key和下标来批量提取value,这是比较困难的,使用jsonpat...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- python计时 (73)
- python安装路径 (56)
- python类型转换 (93)
- python自定义函数 (53)
- python进度条 (67)
- python吧 (67)
- python字典遍历 (54)
- python的for循环 (65)
- python格式化字符串 (61)
- python串口编程 (60)
- python读取文件夹下所有文件 (59)
- java调用python脚本 (56)
- python操作mysql数据库 (66)
- python字典增加键值对 (53)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python人脸识别 (54)
- python多态 (60)
- python命令行参数 (53)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)