本文出自“Python 为什么”系列,请查看全副文章
Python 在波及 真值判断
(Truth Value Testing)时,语法很简便。
比方,在判断某个对象是否不为 None 时,或者判断容器对象是否不为空时,并不需要显示地写出判断条件,只须要在 if 或 while 关键字前面间接写上该对象即可。
下图以列表为例,if my_list
这个简短的写法能够表白出两层意思:
如果须要作出相同的判断,即“如果为 None 或为空”,只须要写成if not my_list
即可。
不同凡响的真值判断形式
通常而言,当一个值自身是布尔类型时,写成 ”if xxx”(如果真),在语义上就很好了解。如果 xxx 自身不是布尔类型时,写成“if xxx”(如果某货色),则在语义上并不好了解。
在 C/C++/Java 之类的 动态语言
中,通常要先基于 xxx 作一个比拟操作,比方“if (xxx == null)”,以此失去一个布尔类型的值的后果,而后再进行真值判断。否则的话,若“if xxx”中有非布尔类型的值,则会报类型谬误。
Python 这门 动静语言
在这种场景中体现出了一种灵活性,那么,咱们的问题来了:为什么 Python 不须要先做一次比拟操作,间接就能对任意对象作真值判断呢?
先来看看文档 中对真值判断的形容:
简略而言,Python 的任何对象都能够用在 if 或 while 或布尔操作(and、or、not)中,默认状况下认为它是 true,除非它有__bool__() 办法返回False
或者有__len__() 办法返回0
。
对于后面的例子,my_list 没有__bool__() 办法,然而它有__len__() 办法,所以它是否为 true,取决于这个办法的返回值。
真值判断的字节码
接着,咱们持续刨根问底:Python 为什么能够反对如此宽泛的真值判断呢?在执行if xxx
这样的语句时,它到底在做些什么?
对于第一个问题,Python 有个内置的 bool() 类型,能够将任意对象转化成布尔值。那么,这是否意味着 Python 在进行真值判断时,会 隐式地
调用 bool() 呢(即转化成if bool(xxx)
)?(答案为否,下文有剖析)
对于第二个问题,能够先用dis
模块来查看下:
POP_JUMP_IF_FALSE
指令对应的是 if 语句那行,它的含意是:
If TOS is false, sets the bytecode counter to _target_. TOS is popped.
如果栈顶元素为 false,则跳转到指标地位。
这里只有跳转动作的形容,仍看不到一个一般对象是如何变成布尔对象的。
Python 在解释器中到底是如何实现真值判断的呢?
真值判断的源码实现
在微信群友 Jo 的帮忙下,我找到了 CPython 的源码(文件:ceval.c、object.c):
能够看出,对于布尔类型的对象(即 Py_True 和 Py_False),代码会进入到疾速解决的分支;而对于其它对象,则会用 PyObject_IsTrue() 计算出一个 int 类型的值。
PyObject_IsTrue() 函数在计算过程中,顺次会获取 nb_bool、mp_length 和 sq_length 的值,对应的应该就是 __bool__() 和 __len__() 这两个魔术办法的返回值。
这个过程就是前文中所援用的官网文档的形容,正是咱们想要找的答案!
另外,对于内置的 bool(),它的外围实现逻辑正是下面的 PyObject_IsTrue() 函数,源码如下(boolobject.c):
所以,Python 在对一般对象作真值判断时,并没有隐式地调用 bool(),相同它调用了一个独立的函数(PyObject_IsTrue()),而这个函数又被 bool() 所应用。
也就是说,bool() 与 if/while 语句对一般对象的真值判断,事实上是基本相同的解决逻辑。 晓得了原理,就会明确if bool(xxx)
这种写法是多此一举的了(我曾见到过)。
至此,咱们曾经答复了前文中提出的问题。
验证真值判断的过程
接下来,有 3 个测试例子,能够作进一步的验证:
你能够暂停而思考下:bool(Test1)
与 bool(Test1())
各是什么后果?而后顺次判断剩下的两个类,后果又会是什么?
揭晓答案:
bool(Test1) # True
bool(Test2) # True
bool(Test3) # True
bool(Test1()) # True
bool(Test2()) # False
bool(Test3()) # True
起因如下:
- 类对象没被实例化时,bool() 不会调用它的 __bool__() 或 __len__() 这两个魔术办法
- 类对象被实例化后,若同时存在 __bool__() 或 __len__() 魔术办法,则 bool() 会先调用 __bool__() 办法(PS:这个办法要求返回值必须为 bool 类型,因而只有有它,就必然不须要再用__len__() 办法来判断虚实)
数字类型如何作真值判断?
除了这 3 个例子,还有一种状况值得验证,那就是 对于数字类型,它们是怎么做真值判断的呢?
咱们能够验证一下数字类型是否领有那两个魔术办法:
hasattr(2020, “__bool__”)
hasattr(2020, “__len__”)
不难验证出,数字领有的是 __bool__() 魔术办法,并没有__len__() 魔术办法,而且所有类型的数字其实被分成了两类:
__bool__()
返回 False:所有示意 0 的数字,例如0
,0.0
,0j
,Decimal(0)
,Fraction(0, 1)
__bool__()
返回 True:所有其它非 0 的数字
文章小结
Python 中 if xxx
这种简便的写法,尽管是正规的真值判断语法,并它但并不合乎惯例的语义。在 C/C++/Java 之类的语言中,要么 xxx 自身是布尔类型的值,要么是一种可返回布尔类型值的操作,然而在 Python 中,这个“xxx”居然还能够是 任意的 Python 对象!
本文通过对文档、字节码和 CPython 解释器的源码逐渐剖析,发现了 Python 的真值判断过程并不简略,能够提炼出以下的几个要点:
- if/while 是隐性的布尔操作符: 它们除了有“判断”虚实的作用,还具备隐式地将一般对象计算出布尔后果的性能。理论的操作是解释器依据“POP_JUMP_IF_FALSE”指令来实现的,其外围逻辑跟内置的 bool() 是共用了一个底层办法
- 真值判断过程依赖两个魔术办法: 除非被判断对象有__bool__() 办法返回
False
或者有__len__() 办法返回0
,否则布尔操作的后果都是 True。两个魔术办法总是会先计算__bool__() - 数字类型也可做真值判断: 数字有__bool__() 魔术办法,但没有__len__() 魔术办法,除了示意 0 的数字为 False,其它数字都为 True
如果你感觉本文剖析得不错,那你应该会喜爱这些文章:
1、Python 为什么应用缩进来划分代码块?
2、Python 的缩进是不是反人类的设计?
3、Python 为什么不必分号作语句终止符?
4、Python 为什么没有 main 函数?为什么我不举荐写 main 函数?
5、Python 为什么举荐蛇形命名法?
6、Python 为什么不反对 i++ 自增语法,不提供 ++ 操作符?
7、Python 为什么只需一条语句“a,b=b,a”,就能间接替换两个变量?
8、Python 为什么用 # 号作正文符?
9、Python 为什么要有 pass 语句?
10、Python 为什么会有个奇怪的“…”对象?
本文属于“Python 为什么”系列(Python 猫出品),该系列次要关注 Python 的语法、设计和倒退等话题,以一个个“为什么”式的问题为切入点,试着展示 Python 的迷人魅力。所有文章将会归档在 Github 上,我的项目地址:https://github.com/chinesehuazhou/python-whydo