Skip to content

Commit

Permalink
布尔值的实现原理
Browse files Browse the repository at this point in the history
  • Loading branch information
satori1995 committed Dec 12, 2024
1 parent 65c2176 commit 6fcdafc
Show file tree
Hide file tree
Showing 2 changed files with 264 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
## 楔子

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

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

## 布尔类型

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

~~~Python
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。

~~~c
// 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。

## 布尔值的底层结构

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

~~~C
// 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 \*,所以这两个结构体实例一般不直接用,而是用底层提供的两个宏。

~~~C
// 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 \* 之后再返回。为此解释器还提供了两个宏。
~~~C
// 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。

~~~C
// 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 类型,将对象转成布尔值。
~~~C
// 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 函数里面,看一下它的内部逻辑。

~~~C
// 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 代码演示一下。
~~~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
~~~

现在你是不是对布尔值有一个更深的印象了呢?一个简单的布尔值,居然有这么多可说的。

但是还没结束,我们还要补充一个知识点,先看一段代码。

~~~python
name = "satori"

if name:
pass

if bool(name):
pass
~~~

这两个 if 判断有啥区别呢?首先 <font color="blue">if bool(name)</font> 我们已经分析过了,它会执行 bool_new 函数,将参数解析出来,接着再调用 PyObject_IsTrue,最后得到布尔值。

而对于 <font color="blue">if name</font> 来说,它会直接调用 PyObject_IsTrue,后续在分析 if 语句的时候会介绍。所以在工作中,我们使用 <font color="blue">if name</font> 即可。

当然啦,获取布尔值除了 <font color="blue">bool(obj)</font> 之外,还可以使用 <font color="blue">not not obj</font>。

~~~Python
name = "satori"

print(bool(name)) # True
print(not not name) # True
~~~

这两者又有什么区别呢?首先 bool(name) 在 Python 里面是一个调用,会进行参数解析,拿到对象之后调用 PyObject_IsTrue 判断真假,正常执行的话,会返回 1 或 0。然后基于 1 和 0 创建布尔值,为 1 返回 True,为 0 返回 False。

而 <font color="blue">not name</font> 会对应一条 UNARY_NOT 字节码,它内部也会调用 PyObject_IsTrue,如果结果为 1 返回 False,为 0 返回 True,正好是相反的。所以 <font color="blue">not not name</font> 则相当于在 <font color="blue">not name</font> 的基础上再反过来一次,这样就和 bool(name) 的结果是一致的了。

当然在工作中,无论使用哪种都可以,看自己喜好。但为了代码的可读性,显式获取布尔值的时候还是建议使用 bool(name),效率上没太大差别。

## 小结

以上就是布尔值相关的内容。

+ bool 继承 int,并且布尔值在底层和整数使用同一个结构体,只是 ob_type 不同;
+ 布尔值具备整数的所有特征,可以像整数一样参与各种运算,其中 True 会被解释成 1,False 会被解释成 0;
+ 布尔值有两种,分别是 True 和 False,它们是单例的。判断时应该使用 is,而不是 ==;
+ 在 Python 里面如果要创建布尔值,有三种方式:通过 True 和 False 字面量、调用类型对象 bool,使用 not not;

-----

&nbsp;

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

![](./images/qrcode_for_gh.jpg)

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

![](./images/supports.png)
3 changes: 2 additions & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@
+ [16. Python 的整数是怎么设计的,为什么它不会溢出?](16.Python的整数是怎么设计的,为什么它不会溢出?.md)
+ [17. 解密 Python 的小整数对象池](17.解密Python的小整数对象池.md)
+ [18. 两个 Python 整数之间是如何进行大小比较的?过程并不像我们想的那样简单](18.两个Python整数之间是如何进行大小比较的?过程并不像我们想的那样简单.md)
+ [19. 探究 Python 整数的加减法,感受大整数的运算哲学与魅力](19.探究Python整数的加减法,感受大整数的运算哲学与魅力.md)
+ [19. 探究 Python 整数的加减法,感受大整数的运算哲学与魅力](19.探究Python整数的加减法,感受大整数的运算哲学与魅力.md)
+ [20. Python 的布尔值是怎么实现的,你对它的了解有多深呢?](20.Python的布尔值是怎么实现的,你对它的了解有多深呢?.md)

0 comments on commit 6fcdafc

Please sign in to comment.