楔子

本篇文章来聊一聊布尔值是怎么实现的,在 Python 里面 True 和 False 虽然是关键字,但它们其实也是对象。

下面我们就来详细地解释一下。

布尔类型

先来说一下布尔类型本身,我们知道 bool 继承自 int,所以 True 和 False 也具备整数的特征。

print(bool.__base__)  # <class 'int'>
print(isinstance(True, bool))  # 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",
    sizeof(struct _longobject),
    0,
    0,                                          /* 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。

布尔值的底层结构

既然 bool 继承 int,那么布尔值和整数的底层结构是一样的。

// Objects/boolobject.c

// PyLongObject 是 struct _longobject 的类型别名
struct _longobject _Py_FalseStruct = {
    PyVarObject_HEAD_INIT(&PyBool_Type, 0)
    { 0 }
};

struct _longobject _Py_TrueStruct = {
    PyVarObject_HEAD_INIT(&PyBool_Type, 1)
    { 1 }
};

我们看到布尔值在底层是静态定义好的 PyLongObject 结构体实例,ob_digit 分别为 [0] 和 [1],所以 False 和 True 完全可以当成 0 和 1 来用。当然啦,由于变量都是 PyObject *,所以这两个结构体实例一般不直接用,而是用底层提供的两个宏。

// Include/boolobject.h

/* Use these macros */
#define Py_False ((PyObject *) &_Py_FalseStruct)
#define Py_True ((PyObject *) &_Py_TrueStruct)

当返回 Python 的 True 和 False 时,底层会返回 Py_True 和 Py_False,也就是转成 PyObject * 之后再返回。为此解释器还提供了两个宏。

// Include/boolobject.h

#define Py_RETURN_TRUE return Py_INCREF(Py_True), Py_True
#define Py_RETURN_FALSE return Py_INCREF(Py_False), Py_False

当然这些应该比较简单了。

布尔值的创建

创建布尔值有两种方式,一种是基于 C 整数创建,另一种是将 Python 对象转成布尔值。

基于 C 整数创建,会通过 PyBool_FromLong 函数,显然它是布尔对象的特定类型 API。

// Objects/boolobject.c

PyObject *PyBool_FromLong(long ok)
{
    PyObject *result;

    if (ok)
        result = Py_True;
    else
        result = Py_False;
    Py_INCREF(result);
    return result;
}

这个特定类型 API 一般都是解释器内部使用,或者编写扩展的时候使用。而除了这种方式,我们还可以调用 bool 类型,将对象转成布尔值。

// Objects/boolobject.c

// 基于 Python 对象创建,比如 bool(obj)
// 显然会调用 PyBool_Type 的 tp_new,在底层该字段被赋值为 bool_new
tatic 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_Islrue 判断 x 是真是假
    // 如果为真返回 1,否则返回 0
    ok = PyObject_IsTrue(x);
    if (ok < 0)
        return NULL;
    // 将整数转成布尔值
    return PyBool_FromLong(ok);
}

所以核心就在 PyObject_IsTrue 函数里面,看一下它的内部逻辑。

// Objects/object.c
int
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 (v->ob_type->tp_as_number != NULL &&
             v->ob_type->tp_as_number->nb_bool != NULL)
        res = (*v->ob_type->tp_as_number->nb_bool)(v);
    // 如果 v 是映射型对象,并且它的类型对象定义了 __len__,那么调用
    // 说白了就是基于内部的键值对个数进行判断
    else if (v->ob_type->tp_as_mapping != NULL &&
             v->ob_type->tp_as_mapping->mp_length != NULL)
        res = (*v->ob_type->tp_as_mapping->mp_length)(v);
    // 如果 v 是序列型对象,并且它的类型对象定义了 __len__,那么调用
    // 也就是基于内部的元素个数进行判断
    else if (v->ob_type->tp_as_sequence != NULL &&
             v->ob_type->tp_as_sequence->sq_length != NULL)
        res = (*v->ob_type->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);
}

当 PyObject_IsTrue 调用完之后,再基于 PyBool_FromLong 创建布尔值即可,我们用 Python 代码演示一下。

# 不传参数,默认返回 False
print(bool())  # False

# 整数实现了 __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,而不是 ==,除非你把 True 和 False 当成整数使用;
  • 在 Python 里面如果要创建布尔值,有三种方式:通过 True 和 False 字面量、调用类型对象 bool、使用 not not;

 

欢迎大家关注我的公众号:古明地觉的编程教室。

如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。