Python里的and和or

Python里的andor可以说是最基础的小白语法了,今天在看Flask源码的时候有几处关于andor的地方却感觉看不太懂,如:

1
2
3
4
5
6
def get_env():                                                      
"""Get the environment the app is running in, indicated by the
:envvar:`FLASK_ENV` environment variable. The default is
``'production'``.
"""
return os.environ.get('FLASK_ENV') or 'production'

我觉得非常奇怪,这不是肯定返回True的吗?

发生了什么

在我脑海中,os.environ.get('FLASK_ENV') or 'production'的执行逻辑应该是这样的:

  1. os.environ.get('FLASK_ENV') 获得一个返回值,如'development'None
  2. 执行隐式的类型转换bool('development')得到True或者bool(None)得到False
  3. or进行短路求值,如果or之前是True,直接返回True
  4. 反之则对or之后的值进行类型转换bool('production')得到True,并返回True
  5. 综上肯定返回True

其实如果合理推断一下,应该能猜出来return os.environ.get('FLASK_ENV') or 'production'的作用是当os.environ.get('FLASK_ENV')的返回值不是None的时候,返回其返回值,否则返回'production',但是这个语法和我之前曾经理解的andor完全不同。我本来觉得这是return里的一个特例,查了一下才发现这是Python的标准语法。

探索and和or

在Python里尝试一些andor的操作,有一些很有意思的结果。首先TrueFalseandor操作是很平凡的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
In [22]: True and True
Out[23]: True

In [23]: True and False
Out[22]: False

In [24]: False and True
Out[25]: False

In [25]: False and False
Out[24]: False

In [26]: True or True
Out[26]: True

In [27]: True or False
Out[27]: True

In [28]: False or True
Out[28]: True

In [29]: False or False
Out[29]: False

然而如果我们不使用布尔值作为运算数(operand),结果就开始显得有点匪夷所思了:

1
2
3
4
5
6
7
8
9
10
11
In [30]: 123 and 321
Out[30]: 321

In [31]: 321 and 123
Out[31]: 123

In [32]: 123 or 321
Out[32]: 123

In [33]: 321 or 123
Out[33]: 321

可以看到,至少and两个int最后得到其中一个int是不符合语义的。以上测试的123和321从布尔值的意义上来说都是True,人类还相对比较好理解一些,如果我们引入False,情况就会显得非常混乱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
In [40]: 123 and 0
Out[40]: 0

In [41]: 0 and 123
Out[41]: 0

In [42]: 123 or 0
Out[42]: 123

In [43]: 0 or 123
Out[43]: 123

In [44]: '' and 0
Out[44]: ''

In [45]: 0 and ''
Out[45]: 0

In [46]: 0 or ''
Out[46]: ''

In [47]: '' or 0
Out[47]: 0

这其中大部分结果都是违反人类直觉的,其中123 or 00 or 123稍好一点——这也是开头我提到Flask源码中的用例,可以找到两个值中为True的部分。从这一系列实验中也可以体会出Python关于andor的特性最别扭的一点是and的返回值只有一个。

官方文档的说法

以上这些测试的结果,虽然直观感受令人疑惑,但抛开语义理性分析不难发现规律,其实官方文档里也做了介绍:

Operation Result
x or y if x is false, then y, else x
x and y if x is false, then x, else y

简单来说,就是短路求值+andor返回其中一个运算数而不是布尔值

了解了这一点之后,我们可以利用把运算数和结果都看成一个布尔值的方式来理解这些运算的结果了,如'' and 123得到''False and True得到False,而'' or 123得到123False or True得到True

我觉得

我所熟悉的语言并不多,简单测试了一下,C/C++中&&||的行为是符合人的预期的。123 && 321返回1123 && 0返回0。所以我到现在还是很震惊Python有这么令人难受的语法。不可否认,这一语法一定程度上允许用更少的代码实现更多的功能,比如现在我希望按照str1 > str2 > str3的顺序找出其中第一个不为空的字符串,可以简单地这样写:

1
ret_str = str1 or str2 or str3

但语义几乎只有有经验的程序员才能正确理解,是对”Explicit is better then implicit”的公然违背。为什么Python要采用这种实现呢?这个问题可能要从Python的字节码和编译器里找答案。假如我有这样一个函数:

1
2
def bar(): 
a = b and c and d and e

编译后“反汇编”得到的结果是:

1
2
3
4
5
6
7
8
9
10
2           0 LOAD_GLOBAL              0 (b)
2 JUMP_IF_FALSE_OR_POP 14
4 LOAD_GLOBAL 1 (c)
6 JUMP_IF_FALSE_OR_POP 14
8 LOAD_GLOBAL 2 (d)
10 JUMP_IF_FALSE_OR_POP 14
12 LOAD_GLOBAL 3 (e)
>> 14 STORE_FAST 0 (a)
16 LOAD_CONST 0 (None)
18 RETURN_VALUE

可以看到,短路求值是通过判断真假后进行jump实现的,而判断真假在解释器(ceval)层面进行,并不存在类似于将b and c转化为bool(b) and bool(c)之类的过程。如果满足jump条件,在解释器栈顶的元素不会做任何修改,也就不存在类型转换的过程。到此为止,Python编译器的实现和其他语言(比如C)还是一致的,不同之处在于Python在jump之后
直接利用栈顶元素对左侧运算符赋值,而其他语言会jump到利用bool值对左侧运算符赋值的代码块。假如说Python也希望这样的行为,编译出的代码应该为:

1
2
3
4
5
6
7
8
9
10
11
12
13
2           0 LOAD_GLOBAL              0 (b)
2 POP_JUMP_IF_FALSE 14
4 LOAD_GLOBAL 1 (c)
6 POP_JUMP_IF_FALSE 14
8 LOAD_GLOBAL 2 (d)
10 POP_JUMP_IF_FALSE 14
12 LOAD_GLOBAL 3 (e)
>> 14 LOAD_CONST 0 (True)
16 JUMP_ABSOLUTE 20
18 LOAD_CONST 1 (False)
>> 20 STORE_FAST 0 (a)
22 LOAD_CONST 2 (None)
24 RETURN_VALUE