阅读 PEP 是理解 Python 特性的绝好方式。Python 3.8 引入了赋值表达式,它是什么?怎么用?有什么限制?话不多说,直接看 PEP。


一、简介

本提案建议在 Python 中增加 := 运算符,使我们可以在表达式中直接赋值给变量。

增加这个运算符后,字典推导式的计算顺序也将作出调整,从而确保 键 的计算先于 值 的计算(因为 键 的值可能会被绑定在一个变量名称上,用于 值 的计算)。

在本提案的讨论过程中,:= 被非正式地称为“海象符”("the walrus operator")。带这种运算符的表达式,正式名称是“赋值表达式”("Assignment Expressions",即本提议的标题),有时也被称为“命名表达式”("Named Expressions",例如 CPython 实现中即以此作为内部名称)。


二、必要性说明

命名某个表达式的结果是编程中的重要一环,使我们只需记住一个简单的名称,而不是一长串的表达式,并且也容易复用。目前,Python 只能在赋值声明中进行命名,因而在列表推导式或一些其它场景下,就无法进行命名。

另外,在交互式 debug 过程中,命名某个大型表达式的一部分可以帮助我们做一些深入的检查。如果无法获取表达式的局部结果,就往往需要在调试过程中重构代码;通过赋值表达式,这些重构将被几个简单的 := 替代。

由于不再需要重构代码,我们在调试过程中不经意地改变代码逻辑的几率也降低了(调试过程中的重构,是导致 海森堡Bug 的常见原因),同时让我们更容易向别的程序员解释程序逻辑。

(译注:所谓 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 = 0partial_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] = xself.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 = positionname, 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 += tzreturn s
  • 改进后:
s = _format_time(self._hour, self._minute,                 self._second, self._microsecond,                 timespec)if tz := self._tzstr():    s += tzreturn 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 codeelse:    # Child code

五、代码风格建议

有些地方可以等效地使用赋值表达式与赋值声明,那么,应该优先使用哪一种呢?我们有以下两条建议:

  1. 如果可以,优先使用赋值声明,它可以更清楚地表明意图。
  2. 如果使用赋值表达式可能导致计算顺序不明确,应重构为使用赋值声明的代码。

(译注:本提案还有 3 个附录,本文已经较长,之后再翻译推荐给大家,请多多见谅!)

stata行业变量怎么赋值_PEP572:赋值表达式(海象符)相关推荐

  1. stata行业变量怎么赋值_免费视频教程!零基础学Python系列(3) 理解“变量”

    点击 跟哥一起学Python 关注我们 python变量(上) python变量(下) 实例源码 本节视频涉及到的实例源码,可以在百度网盘中下载.在公众号对话框回复关键字"网盘地址" ...

  2. stata行业变量怎么赋值_邹军:怎么通过宏程序实现刀具寿命管理(二)

    原标题:邹军:怎么通过宏程序实现刀具寿命管理(二) [邹军,十多年数控工作经验,现自创一套有理论,有干货,还有方法论做支撑的实战编程教程(PDF),从而让你编写程序就像做填空题一样简单.在没有人指引你 ...

  3. stata行业变量怎么赋值_动态面板模型估计方法简介以及stata应用

    动态面板模型最主要的特征是在控制变量中加入了y的滞后项 , 其中 代表控制变量, 代表固定效应, 代表残差项 由于加入了y的滞后项导致了采用常用固定效应估计方法会导致参数估计的不一致性,因此需要采用其 ...

  4. stata行业变量怎么赋值_Tobit 模型及其Stata实现

    作者:李琼琼 (山东大学) Stata 连享会: 知乎 | 简书 | 码云 | CSDN | StataChina公众号 连享会-知乎推文列表 Note: 助教招聘信息请进入「课程主页」查看. 因果推 ...

  5. json解析 子类和父类同名属性如何赋值_想学变量的解构赋值?看完这一篇就够了...

    序言 ES6允许按照一定模式从数组和对象中提取值,然后对变量进行复制,这被称为解构(Destructuring) 数组的解构赋值 基本用法 像上面的例子,可以从数组中提取值,按照对应位置对变量赋值,这 ...

  6. ES6的新特性(3)——变量的解构赋值

    变量的解构赋值 数组的解构赋值 基本用法 ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring). let a = 1; let b = 2; le ...

  7. ECMAScript 6入门 - 变量的解构赋值

    定义 ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring). 解构赋值不仅适用于var命令,也适用于let和const命令. 解构赋值的规则是,只要 ...

  8. php mysql变量赋值给变量_MySQL_mysql 存储过程中变量的定义与赋值操作,一、变量的定义 mysql中变量定 - phpStudy...

    mysql 存储过程中变量的定义与赋值操作 一.变量的定义mysql中变量定义用declare来定义一局部变量,该变量的使用范围只能在begin...end 块中使用,变量必须定义在复合语句的开头,并 ...

  9. [ES6] 细化ES6之 -- 变量的解构赋值

    变量的解构赋值 解构赋值是什么 ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值 var/let [变量名称1,变量名称2,...] = 数组或对象 本质上,这种写法属于"模式 ...

最新文章

  1. 循环斐波那契数列_剑指offer #10 斐波那契数列
  2. AI开放只是幌子?科技巨头边承诺开放边申请专利
  3. “计算机艺术之父”、现代计算机技术先驱查理斯·苏黎去世,享年99岁!
  4. 一篇文章让你真正了解Java
  5. ChannelFactory.Endpoint 上的地址属性为空。ChannelFactory 的终结点必须指定一个有效的地址。...
  6. C# 使用Win32 API模拟键盘鼠标操作网页
  7. c语言中的两个百分号什么意思,百分号的用法,特别是在两个量词之间的用法,例如50%—70%和50—70%...-百分号-语文-彭都宰同学...
  8. 信息学奥赛一本通 1164:digit函数
  9. Form窗体的属性与值 1123
  10. 阿里云服务器ECS选型
  11. DB查询语句的编写和执行顺序
  12. 查看web站点缓存的好工具Cache Manager -- 避免滥用缓存
  13. OpenWrt running on VMware
  14. Mysql闪退无法打开,试试这个方法
  15. gimp 架构_GIMP添加插件..doc
  16. Word文档乱码恢复操作----免费指导
  17. JavaScript系列之高级篇(2)
  18. 简单的连数据库 拼接数据 发邮件Python脚本
  19. IV油管套件和配件的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告
  20. 一张图,理清微服务架构路线(收藏)

热门文章

  1. Java开源的ERP系统源码带文字搭建教程,前后端分离架构
  2. 中国人工智能行业发展状况与竞争格局分析报告2022-2028年版
  3. arduino花盆含水量_Arduino教程┃模拟土壤湿度传感器的介绍
  4. 黑马Android开发视频教程125集
  5. 西游记团队中如果需要裁掉一个人,会先裁掉谁?
  6. 国内首个 IoT 物联网平台建设白皮书
  7. word缺失字体问题
  8. 【开源项目】用ESP32制作一个桌面天气预报站
  9. [论文笔记]A ConvNet for the 2020s
  10. CEAC 之《企业信息化管理》3