Python里的and
和or
可以说是最基础的小白语法了,今天在看Flask
源码的时候有几处关于and
和or
的地方却感觉看不太懂,如:1
2
3
4
5
6def 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'
的执行逻辑应该是这样的:
os.environ.get('FLASK_ENV')
获得一个返回值,如'development'
或None
- 执行隐式的类型转换
bool('development')
得到True
或者bool(None)
得到False
- 对
or
进行短路求值,如果or
之前是True
,直接返回True
- 反之则对
or
之后的值进行类型转换bool('production')
得到True
,并返回True
- 综上肯定返回True
其实如果合理推断一下,应该能猜出来return os.environ.get('FLASK_ENV') or 'production'
的作用是当os.environ.get('FLASK_ENV')
的返回值不是None
的时候,返回其返回值,否则返回'production'
,但是这个语法和我之前曾经理解的and
和or
完全不同。我本来觉得这是return
里的一个特例,查了一下才发现这是Python的标准语法。
探索and和or
在Python里尝试一些and
和or
的操作,有一些很有意思的结果。首先True
和False
的and
和or
操作是很平凡的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23In [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
11In [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
23In [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 0
和0 or 123
稍好一点——这也是开头我提到Flask
源码中的用例,可以找到两个值中为True
的部分。从这一系列实验中也可以体会出Python关于and
和or
的特性最别扭的一点是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 |
简单来说,就是短路求值+and
和or
返回其中一个运算数而不是布尔值。
了解了这一点之后,我们可以利用把运算数和结果都看成一个布尔值的方式来理解这些运算的结果了,如'' and 123
得到''
即False and True
得到False
,而'' or 123
得到123
即False or True
得到True
。
我觉得
我所熟悉的语言并不多,简单测试了一下,C/C++中&&
和||
的行为是符合人的预期的。123 && 321
返回1
,123 && 0
返回0
。所以我到现在还是很震惊Python有这么令人难受的语法。不可否认,这一语法一定程度上允许用更少的代码实现更多的功能,比如现在我希望按照str1 > str2 > str3
的顺序找出其中第一个不为空的字符串,可以简单地这样写:1
ret_str = str1 or str2 or str3
但语义几乎只有有经验的程序员才能正确理解,是对”Explicit is better then implicit”的公然违背。为什么Python要采用这种实现呢?这个问题可能要从Python的字节码和编译器里找答案。假如我有这样一个函数:1
2def bar():
a = b and c and d and e
编译后“反汇编”得到的结果是:1
2
3
4
5
6
7
8
9
102 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
132 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