楔子
本篇文章来聊一聊布尔值是怎么实现的,在 Python 里面 True 和 False 虽然是关键字,但它们其实也是对象。
下面我们就来详细地解释一下。
布尔类型
先来说一下布尔类型本身,我们知道 bool 继承自 int,所以 True 和 False 也具备整数的特征。
print(bool.__base__) # <class 'int'>
print(isinstance(True, int)) # True
# True 和 False 可以当成 1 和 0 来用
print(True * 2) # 2
print(3 // 3 == True) # True
print(sum([True, 1, 2])) # 4
print(False + 1) # 1
bool 在底层对应 PyBool_Type,因此我们可以肯定地讲,PyBool_Type 的 tp_base 字段的值一定是 &PyLong_Type。
// Objects/boolobject.c
PyTypeObject PyBool_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"bool",
offsetof(struct _longobject, long_value.ob_digit), /* tp_basicsize */
sizeof(digit), /* tp_itemsize */
bool_dealloc, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
bool_repr, /* tp_repr */
&bool_as_number, /* tp_as_number */
// ...
&PyLong_Type, /* tp_base */
// ...
};
bool 继承 int,所以它也实现了 tp_as_number。
布尔值的底层结构
然后是 True 和 False,既然 bool 继承 int,那么布尔值和整数的底层结构是一样的。
// Objects/boolobject.c
// PyLongObject 是 struct _longobject 的类型别名
struct _longobject _Py_FalseStruct = {
PyObject_HEAD_INIT(&PyBool_Type)
{ .lv_tag = _PyLong_FALSE_TAG,
{ 0 }
}
};
struct _longobject _Py_TrueStruct = {
PyObject_HEAD_INIT(&PyBool_Type)
{ .lv_tag = _PyLong_TRUE_TAG,
{ 1 }
}
};
我们看到布尔值在底层是静态定义好的 PyLongObject 结构体实例,ob_digit 分别为 [0] 和 [1],所以 False 和 True 完全可以当成 0 和 1 来用。
当然啦,由于变量都是 PyObject *,所以这两个结构体实例一般不直接用,而是用底层提供的两个宏。
// Include/boolobject.h
/* Use these macros */
#define Py_False _PyObject_CAST(&_Py_FalseStruct)
#define Py_True _PyObject_CAST(&_Py_TrueStruct)
当返回 Python 的 True 和 False 时,底层会返回 Py_True 和 Py_False,也就是转成 PyObject * 之后再返回。为此解释器还提供了两个宏。
// Include/boolobject.h
/* Macros for returning Py_True or Py_False, respectively */
#define Py_RETURN_TRUE return Py_True
#define Py_RETURN_FALSE return Py_False
当然这些应该比较简单了。
布尔值的创建
创建布尔值有两种方式,一种是基于 C 整数创建,另一种是将 Python 对象转成布尔值。
基于 C 整数创建,会通过 PyBool_FromLong 函数,显然它是布尔对象的特定类型 API。
// Objects/boolobject.c
// 基于 C 整数创建,对于 C 来说,整数 0 为假,非 0 为真
PyObject *PyBool_FromLong(long ok)
{
return ok ? Py_True : Py_False;
}
这个特定类型 API 一般都是解释器内部使用,或者编写扩展的时候使用。而除了这种方式,我们还可以调用 bool 将对象转成布尔值。
// Objects/boolobject.c
// 基于 Python 对象创建,比如 bool(obj)
static PyObject *
bool_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
// 保存接收的参数,先设置为 False
PyObject *x = Py_False;
long ok;
// bool 类型不接收关键字参数
if (!_PyArg_NoKeywords("bool", kwds))
return NULL;
// 最多接收 1 个位置参数,解析出来赋值给 x
// 如果不传位置参数,那么 x 就是上面设置的 Py_False
if (!PyArg_UnpackTuple(args, "bool", 0, 1, &x))
return NULL;
// 调用 PyObject_IsTrue 判断 x 是真是假
// 如果为真返回 1,否则返回 0
ok = PyObject_IsTrue(x);
if (ok < 0)
return NULL;
// 将整数转成布尔值
return PyBool_FromLong(ok);
}
所以核心就在 PyObject_IsTrue 函数里面,看一下它的内部逻辑。
// Objects/object.c
PyObject_IsTrue(PyObject *v)
{
Py_ssize_t res;
// 如果 v 是 True,返回 1
if (v == Py_True)
return 1;
// 如果 v 是 False,返回 0
if (v == Py_False)
return 0;
// 如果 v 是 None,返回 0
if (v == Py_None)
return 0;
// 如果 v 是数值型对象,并且它的类型对象定义了 __bool__,那么调用
else if (Py_TYPE(v)->tp_as_number != NULL &&
Py_TYPE(v)->tp_as_number->nb_bool != NULL)
res = (*Py_TYPE(v)->tp_as_number->nb_bool)(v);
// 如果 v 是映射型对象,并且它的类型对象定义了 __len__,那么调用
// 说白了就是基于内部键值对的个数进行判断
else if (Py_TYPE(v)->tp_as_mapping != NULL &&
Py_TYPE(v)->tp_as_mapping->mp_length != NULL)
res = (*Py_TYPE(v)->tp_as_mapping->mp_length)(v);
// 如果 v 是序列型对象,并且它的类型对象定义了 __len__,那么调用
// 也就是基于内部的元素个数进行判断
else if (Py_TYPE(v)->tp_as_sequence != NULL &&
Py_TYPE(v)->tp_as_sequence->sq_length != NULL)
res = (*Py_TYPE(v)->tp_as_sequence->sq_length)(v);
// 否则默认为真,比如我们自定义类的实例
else
return 1;
// 如果 res 大于 0,返回 1,否则返回 0
return (res > 0) ? 1 : Py_SAFE_DOWNCAST(res, Py_ssize_t, int);
}
我们用 Python 代码演示一下。
# 整数实现了 __bool__,所以 bool(1) 会调用 int.__bool__(1)
print(bool(1)) # True
# 字符串实现了 __len__,所以 bool("abc") 会调用 str.__len__("abc")
print(bool("abc")) # True
class A:
pass
# 自定义类没有实现 __bool__、__len__
# 所以在 PyObject_IsTrue 里面最终会走 else 分支,直接为真
print(bool(A())) # True
# 但如果定义了 __len__,那么是否为真取决于 __len__ 的返回值
# 并且 __len__ 要定义在类型对象里面,因为 a.__len__() 其实只是语法糖
# 底层真正执行的是 A.__len__(a),关于这部分细节后续介绍类的时候会细说
type.__setattr__(A, "__len__", lambda self: 0)
# 因为 __len__ 返回的是 0,所以为假,注意:__len__ 要返回整数
print(bool(A())) # False
# 如果再实现一个 __bool__ 呢?
type.__setattr__(A, "__bool__", lambda self: True)
# 我们发现结果又变成了 True,因为 __bool__ 返回的是 True(必须返回布尔值)
# 并且在源码中,__bool__ 查找的优先级高于 __len__
print(bool(A())) # True
现在你是不是对布尔值有一个更深的印象了呢?一个简单的布尔值,居然有这么多可说的。
但是还没结束,我们还要补充一个知识点,先看一段代码。
name = "satori"
if name:
pass
if bool(name):
pass
这两个 if 判断有啥区别呢?首先 if bool(name) 我们已经分析过了,它会执行 bool_new 函数,将参数解析出来,接着再调用 PyObject_IsTrue,最后得到布尔值。
而对于 if name 来说,它会直接调用 PyObject_IsTrue,后续在分析 if 语句的时候会介绍。所以在工作中,我们使用 if name 这种形式就好。
当然啦,获取布尔值除了 bool(obj) 之外,还可以使用 not not obj。
name = "satori"
print(bool(name)) # True
print(not not name) # True
这两者又有什么区别呢?首先 bool(name) 在 Python 里面是一个调用,会进行参数解析,拿到对象之后调用 PyObject_IsTrue 判断真假,正常执行的话,会返回 1 或 0。然后基于 1 和 0 创建布尔值,为 1 返回 True,为 0 返回 False。
而 not name 会对应一条 UNARY_NOT 字节码,它内部也会调用 PyObject_IsTrue,如果结果为 1 返回 False,为 0 返回 True,正好是相反的。所以 not not name 则相当于在 not name 的基础上再反过来一次,这样就和 bool(name) 的结果是一致的了。
当然在工作中,无论使用哪种都可以,看自己喜好。但为了代码的可读性,显式获取布尔值的时候还是建议使用 bool(name),效率上没太大差别。
小结
以上就是布尔值相关的内容。
bool 继承 int,并且布尔值在底层和整数使用同一个结构体,只是 ob_type 不同;
布尔值具备整数的所有特征,可以像整数一样参与各种运算,其中 True 会被解释成 1,False 会被解释成 0;
布尔值有两种,分别是 True 和 False,它们是永恒对象,并且是单例的。判断时应该使用 is,而不是 ==;