前言

分享一个在 Twitter 上看到的使用海象运算符的例子,觉得挺有意思的,代码是这样的:

>>> (a := 1)
>>> (a, b := 2, 3)
>>> print(f'a={a}, b={b}')
a=1, b=2

里面这句(a, b := 2, 3)特别有迷惑性,尤其是你写过 Go,很可能直觉的认为这个表达式没有问题。但是通过输出可以看到根本不符合预期,那么到底是哪里不对呢?我们慢慢的拆解一下

为什么要加圆括号

这个话题扩展起来很大,涉及到 Python 语法,我们逐步深入。

表达式和语句的区别

我们写的程序就是由一个或者多个语句组成的,语句 (Statement) 是一行或者多行代码,是整个程序的一个独立单元。而表达式 (Expression) 是一个特殊的语句,它只能包含标识符 (字母、数字、下划线等)、字面量 (Python 内置常量类型,如字符串、数字、浮点数) 和运算符(加减乘除、大于小于等于、异或、取余等):

1 + 2  # 表达式
a = 1 + 2  # 语句

如上面的例子,1 + 2本质上是求值 (等于 3),a = 1 + 2只表示代码逻辑,没有求值,只是个赋值语句

赋值表达式和赋值语句

我们在看一个例子:

In : x = 1

In : x
Out: 1

In : (y := 2)
Out: 2

In : y
Out: 2

其中x = 1是一个赋值语句 (assignment statement),这样会将一个特定的值 (1) 设置到某个特定的存储地址去,这个位置被标记成一个特定的变量名称 (x)。而(y := 2)是一个赋值表达式 (assignment expression),它比赋值语句多加了求值这一步,也就是返回了结果 (2),所以可以看到Out: 2这个输出。

而不加圆括号是语法错误:

In : y := 2
  File "<ipython-input-8-b0043aac4290>", line 1
    y := 2
      ^
SyntaxError: invalid syntax

这是因为在 Python 语言里,赋值表达式和赋值语句是不同的语法,下面 2 种方式是正确语法:

  1. 赋值表达式使用 := 操作符
  2. 赋值语句使用 = 操作符

同样的,下面的代码也是语法错误:

In : (y = 2)
  File "<ipython-input-13-a73f2c6719f5>", line 1
    (y = 2)
       ^
SyntaxError: invalid syntax

所以不能直接想以:=操作符的方式替代=操作符,必须加一个括号来使用赋值表达式赋单个的值。

赋值语句转化成表达式的问题

刚才说赋值表达式和赋值语句语法不同,这么设计是因为将已经存在的赋值语句转化成表达式是容易出 Bug 的,这个在 C 语言里面就暴露的很明显,举个例子:

#include <stdio.h>

int main() {
  int x = 3, y = 8;

  if (x = y) {
    printf("x and y are equal (x = %d, y = %d)", x, y);
  }
  return 0;
}

这是一段合规的代码,但是(x = y)会让 x 被重新赋值为 y 的值造成这个判断为真,输出结果是x and y are equal (x = 8, y = 8)。原因是代码中并没有用用于比较的操作符==,这种错误很隐蔽,而 Python 或者 Go 等现在编程语言都是直接明确的抛出语法错误:

In : x = 1
...: y = 2
...: if x = y:
...:     print('equal')
  File "<ipython-input-5-5f6807f1b35f>", line 3
    if x = y:
         ^
SyntaxError: invalid syntax

换用正确的海象操作符是可以达到这样的逻辑的 (虽然没必要):

In : x = 1
...: y = 2
...: if (x := y):
...:     print(f'equal: {x=} {y=}')
...:
equal: x=2 y=2

为什么写了一句(a := 1)

答案也就在这里,这个画蛇添足的点就是这段混乱代码的问题所在。其实很简单,试试去掉它:

In : (a, b := 2, 3)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-2-83c3cb14fd64> in <module>
----> 1 (a, b := 2, 3)

NameError: name 'a' is not defined

也就是说这句代码本身写的就不对。其实这个语句表示了一个元组,他有三个元素,分别是 a,b := 2, 3。这里说 a 没有被定义,当然啦,之前就没赋值过 a。在 IPython 中执行,就能理解了:

In : (a := 1)  # 相当于`a = 1`
Out: 1  # 赋值a=1,并且返回这个值(1)

In : (a, b := 2, 3)
Out: (1, 2, 3)

输出其实就是就是返回输入的数据的结果,所以(a, b := 2, 3)返回值是一个元组,第一个元素 a 是前面赋值的结果,第二个元素是海象操作符求值的结果,第三个就是字面量 3。

延伸阅读

  1. https://stackoverflow.com/questions/4728073/what-is-the-difference-between-an-expression-and-a-statement-in-python
  2. https://www.dongwm.com/post/pep-572/
  3. https://realpython.com/python-walrus-operator/
  4. https://docs.python.org/3/reference/expressions.html