Python 比较运算符展开的隐藏坑
Python 比较运算符有个很有简便的用法,即:1 < x < 10
,表示判断 x 是否满足在 (1, 10)
之间,通常被人称为 链式比较
。最近发现了一个隐藏的坑点,相当有意思。
注意:在本文开始之前需要说明的是,以下内容均在 3.x
测试,2.x
不一定适用。
一个例子 🌰
问,按照你以前对 Python 的了解,以下语句中,哪些是 True
,哪些是 False
?(答案放在本文最下面,放心大胆往下看)
1 |
|
比较运算符的展开
文章一开始提到,1 < x < 10
,表示判断 x 是否满足在 (1, 10)
之间。那么 Python 到底是怎么处理这个展开的呢?我们可以用 dis
一探究竟:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17In [1]: from dis import dis
In [2]: x = 5
In [3]: dis('1 < x < 10')
1 0 LOAD_CONST 0 (1)
2 LOAD_NAME 0 (x)
4 DUP_TOP
6 ROT_THREE
8 COMPARE_OP 0 (<)
10 JUMP_IF_FALSE_OR_POP 18
12 LOAD_CONST 1 (10)
14 COMPARE_OP 0 (<)
16 RETURN_VALUE
>> 18 ROT_TWO
20 POP_TOP
22 RETURN_VALUE
Python 代码首先会被“编译“为字节码,然后再由 Python 虚拟机来执行字节码(Python 的字节码是一种类似汇编指令的中间语言,一个 Python 语句会对应若干字节码指令)。dis
这个内置模块是 Python 字节码“反汇编器“,可以帮助我们理解 Python 的代码是怎么执行的。
那么怎么看这个指令呢?比如上面这段结果,以第一行为例:
- 第一列的数字
1
:表示其对应的源代码的行数 - 第二列的
0
:表示字节码的索引
,指令LOAD_CONST
在0
位置 - 第三列:可读性较高的指令名称,告诉我们这个指令是啥意思
- 第四列:表示指令参数所在的位置,我的理解是在堆栈里的位置。
- 第五列:是参数值。
- 上面的结果中,还有个
>>
:表示跳转的目标,会配合类似JUMP
的指令使用。比如 index 为 10 的指令,JUMP_IF_FALSE_OR_POP
,后面跟着参数是18
,表明了跳转到索引为 18 的指令,即ROT_TWO
若有疑问或者想查阅所有的指令,可以查看官方文档:传送门🚪
那么现在终于可以一探究竟了:
0 LOAD_CONST
:将1
压入栈顶。此时堆栈为:顶[ 1 ]底
2 LOAD_NAME
:将x
压入栈顶。此时堆栈为:顶[ x, 1 ]底
4 DUP_TOP
:复制堆栈顶部的引用。此时堆栈为:顶[ x, x, 1 ]底
6 ROT_THREE
:将第二个和第三个堆栈项向上提升一个位置,原来的顶项移动到位置三。此时堆栈为:顶[ x, 1, x ]底
8 COMPARE_OP
:执行布尔运算操作,这里就是<
,执行完之后会把结果压回堆栈,而1 < x
即1<5
为True
。此时堆栈为:顶[ True, x ]底
10 JUMP_IF_FALSE_OR_POP
:如果堆栈顶部为False
就跳,否则就把堆栈顶部弹出,这里明显是要弹出。此时堆栈为:顶[ x ]底
。12 LOAD_CONST
:压入10
。此时堆栈为:顶[ 10, x ]底
。14 COMPARE_OP
:执行比较x < 10
。此时堆栈为:顶[ True ]底
。16 RETURN_VALUE
:返回True
,结束
可以看出,形如 1 < x < 10
的语句,Python 会利用 DUP_TOP
和 ROT_THREE
展开为 1 < x and x < 10
。为什么是 and
呢?从上面的 JUMP_IF_FALSE_OR_POP
可以看出,这是短路运算,如果前面的是 False
就直接返回 False
了,所以是 and
不是 or
。
我们也可以看一下 1 < x and x < 10
的字节码来验证我们的想法:
1
2
3
4
5
6
7
8
9In [1]: dis('1 < x and x < 10')
1 0 LOAD_CONST 0 (1)
2 LOAD_NAME 0 (x)
4 COMPARE_OP 0 (<)
6 JUMP_IF_FALSE_OR_POP 14
8 LOAD_NAME 0 (x)
10 LOAD_CONST 1 (10)
12 COMPARE_OP 0 (<)
>> 14 RETURN_VALUE
是不是很像?
坑点在哪里
在查找 COMPARE_OP
的时候,官方文档有一句话:
cmp_op[opname]
在哪呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16In [1]: import dis
In [2]: dis.cmp_op
Out[2]:
('<',
'<=',
'==',
'!=',
'>',
'>=',
'in',
'not in',
'is',
'is not',
'exception match',
'BAD')
可以看到,我们常用的链式比较符都在里面,但是还有一些稍微有点特殊的:
(not) in
、is (not)
这也就引出了前面那几个语句执行方式,实际上也是会被展开的,以 1 in (1, 2) == True
为例:
1
2
3
4
5
6
7
8
9
10
11
12
13In [94]: dis('1 in (1, 2) == True')
1 0 LOAD_CONST 0 (1)
2 LOAD_CONST 1 ((1, 2))
4 DUP_TOP
6 ROT_THREE
8 COMPARE_OP 6 (in)
10 JUMP_IF_FALSE_OR_POP 18
12 LOAD_CONST 2 (True)
14 COMPARE_OP 2 (==)
16 RETURN_VALUE
>> 18 ROT_TWO
20 POP_TOP
22 RETURN_VALUE
简直一模一样,1 in (1, 2) == True
会被展开成 1 in (1, 2) and (1, 2) == True
。
答案
想必到这里,你应该可以看出,开头的那几个语句均为 False
了吧~
来呀快活呀