通过 type 和 object 之间的关联,进一步分析类型对象

简介: 通过 type 和 object 之间的关联,进一步分析类型对象

楔子



type 和 object 两者的关系估计会让很多人感到困惑,我们说 type 站在类型金字塔的顶端,任何对象按照类型追根溯源,最终得到的都是 type。而 object 站在继承金字塔的顶端,任何类型对象按照继承关系追根溯源,最终得到的都是 object。

因此我们可以得出以下结论:


  • type 的父类是 object
  • object 的类型是 type

验证一下:

print(type.__base__)  # <class 'object'>
print(object.__class__)  # <class 'type'>

打印结果说明结论正确,但这就奇怪了,type 的父类是 object,而 object 的类型又是 type,那么问题来了,是先有 type 还是先有 object 呢?带着这些疑问,开始下面的内容。


类是由谁创建的



首先必须要澄清一个事实,类对象的类型是 type,这句话是没有问题的。但如果说类对象都是由 type 创建的,就有些争议了。因为 type 能够创建的是自定义的类,而内置的类在底层是预先定义好的。

# int、tuple、dict 等内置类型
# 在底层是预先定义好的,以全局变量的形式存在
# 我们直接就可以拿来用
print(int)  # <class 'int'>
print(tuple)  # <class 'tuple'>
# 但对于自定义的类,显然就需要在运行时动态创建了
# 而创建这一过程,就交给 type 来做
class Girl:
    pass

然后 type 也只能对自定义类进行属性上的增删改,内置的类则不行。

class Girl:
    pass
# 给类对象增加一个成员函数
type.__setattr__(
    Girl,
    "info",
    lambda self: "name: 古明地觉, age: 17"
)
# 实例化之后就可以调用了
print(Girl().info())  # name: 古明地觉, age: 17
# 但内置的类对象,type 是无法修改的
try:
    type.__setattr__(int, "a", "b")
except TypeError as e:
    print(e)
"""
TypeError: cannot set 'a' attribute of immutable type 'int'
"""

上一篇文章中我们说了,Python 所有的类型对象(包括 type)都是由 PyTypeObject 结构体实例化得到的,只不过结构体字段的值不同,得到的类也不同。并且内置的类型对象在底层是预定义好的,它们在解释器看来是同级别的,不存在谁创建谁。

而每一个对象都有引用计数和类型,然后解释器将这些类对象的类型都设置成了 type,我们举例说明。不过在此之前,需要先说一个宏。

// Include/object.h
// _PyObject_EXTRA_INIT 可以忽略掉
// 然后我们看到这个宏是用来初始化引用计数和类型的
// 并且引用计数的值为 uint32 类型的最大值,因此创建的是永恒对象
#define PyObject_HEAD_INIT(type)    \
    {                               \
        _PyObject_EXTRA_INIT        \
        { _Py_IMMORTAL_REFCNT },    \
        (type)                      \
    },
    
// 用于初始化引用计数、类型和 ob_size
#define PyVarObject_HEAD_INIT(type, size) \
    {                                     \
        PyObject_HEAD_INIT(type)          \
        (size)                            \
    },

面我们来看几个类型对象。 

08e50e6bd68d48744544cb433e28e0fe.png

我们看到所有类型对象的类型都被设置成了 &PyType_Type,也就是 Python 里的 type。所以结论很清晰了,虽然内置的类型对象可以看做是 type 的实例对象,但它却不是由 type 实例化得到的,而是在底层预定义好,并以全局变量的形式静态出现。

所以内置的类型对象之间不存在谁创建谁,它们都是预定义好的,只是在定义的时候,将自身的类型设置成了 type 而已,包括 type 本身(类型还是 type)。这样一来,每一个对象都会具有一个类型,从而将面向对象理念贯彻的更加彻底。

print(int.__class__)
print(tuple.__class__)
print(set.__class__)
print(type.__class__)
"""
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
"""
print(
    type.__class__.__class__.__class__ is type
)  # True
print(
    type(type(type(type(type(type))))) is type
)  # True

好,说完了这些之后我们来正式考察 type 和 object 的底层实现。


类型对象的类型:PyType_Type



type 是所有类型对象的类型,我们称之为元类型或者元类,即 metaclass,当然它同时也是一个类型对象。下面看一下它的底层实现。

// Objects/typeobject.c
PyTypeObject PyType_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "type",                                     /* tp_name */
    sizeof(PyHeapTypeObject),                   /* tp_basicsize */
    sizeof(PyMemberDef),                        /* tp_itemsize */
    (destructor)type_dealloc,                   /* tp_dealloc */
    offsetof(PyTypeObject, tp_vectorcall),      /* tp_vectorcall_offset */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_as_async */
    (reprfunc)type_repr,                        /* tp_repr */
    &type_as_number,                            /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    0,                                          /* tp_hash */
    (ternaryfunc)type_call,                     /* tp_call */
    // ...
};

所有的类型对象加上元类都是由 PyTypeObject 这个结构体实例化得到的,所以它们内部的字段都是一样的。只不过传入的值不同,实例化之后得到的结果也不同,可以是 PyLong_Type、可以是 PyFloat_Type,也可以是这里的 PyType_Type。

再看一下里面的宏 PyVarObject_HEAD_INIT,它用来初始化引用计数、类型和 ob_size,其中类型被初始化成了 &PyType_Type。换句话说,PyType_Type 里面的 ob_type 字段指向的还是 PyType_Type,而对应 Python 的话,就是 type 的类型还是 type。

>>> type.__class__
<class 'type'>
>>> type.__class__.__class__.__class__.__class__.__class__ is type
True
>>> type(type(type(type(type(type))))) is type
True

显然不管套娃多少次,最终的结果都是True,这也是符合预期的。


类型对象的基类:PyBaseObject_Type



Python 中有两个类型对象比较特殊,一个是站在类型金字塔顶端的 type,另一个是站在继承金字塔顶端的 object。看完了 type,再来看看 object。

由于 object 的类型是 type,那么在初始化 PyBaseObject_Type 的时候,它的 ob_type 一定也被设置成了 &PyType_Type。

我们看一下 PyBaseObject_Type 的实现,它同样定义在 Objects/typeobject.c 中。

245a8409fb53bd64e607f2023a19d65f.png

类型对象在创建的时候,ob_type 字段都会被初始化成 &PyType_Type,而 object 也不例外,所以它的类型为 type,这个非常简单。但 type 的基类是 object,又是怎么一回事呢?

之前介绍类型对象的时候,我们说类型对象内部的 tp_base 表示继承的基类,那么对于 PyType_Type 来讲,它内部的 tp_base 肯定是 &PyBaseObject_Type,即 object。

1c820bf7c1d64b00694ae6dffe794ac8.png

但令我们吃鲸的是,它的 tp_base 居然是个 0,如果为 0 的话则表示没有这个属性,或者说基类为空。不是说 type 的基类是 object 吗?为啥 tp_base 是 0 呢。

事实上如果你去看其它类型的话,会发现它们内部的 tp_base 也是 0。为 0 的原因就在于我们目前看到的类型对象还不够完善,因为 Python 的动态性,显然不可能在定义的时候就将所有字段属性都设置好、然后解释器一启动就得到我们平时使用的类型对象。

因此目前看到的类型对象还不是最终形态,有一部分字段属性是在解释器启动之后再动态完善的,而这个完善的过程被称为类型对象的初始化,它由函数 PyType_Ready 负责。

718a555d4f40a6434f59be6f5b3eee59.png

首先代码中的 type 只是一个普通的参数,当解释器发现一个类对象还没有初始化时,会将其作为参数传递给 PyType_Ready,进行初始化。

初始化过程会做很多的工作,用于完善类型对象,而其中一项工作就是设置基类。如果发现类型对象的基类为空,那么就将基类设置为 object,因为在 Python3 里面新式类都要继承 object。当然啦,这个类不能是 object 本身,object 的基类是 None,因为继承链向上要有一个终点。

当 PyType_Ready 完成初始化之后,就得到我们平常使用的类型对象了,最终 PyType_Type 和 PyBaseObject_Type 的关系如下。

93bc2d7948b1bb6828f1b75ac97c2edf.png

因此到目前为止,type 和 object 之间的恩怨纠葛算是真相大白了,总结一下:

1)和自定义类不同,内置的类不是由 type 实例化得到的,它们都是在底层预先定义好的,不存在谁创建谁。只是内置的类在定义的时候,它们的类型都被设置成了 type。这样不管是内置的类,还是自定义类,在调用时都会执行 type 的 __call__ 函数,从而让它们的行为是一致的。

2)虽然内置的类在底层预定义好了,但还有一些瑕疵,因为有一部分逻辑无法以源码的形式体现,只能在解释器启动的时候再动态完善。而这个完善的过程,便包含了基类的填充,会将基类设置成 object。

所以 type 和 object 是同时出现的,它们的存在需要依赖彼此。首先这两者会以不完全体的形式定义在源码中,并且在定义的时候将 object 的类型设置成 type;然后当解释器启动的时候,再经过动态完善,进化成完全体,而进化的过程中会将 type 的基类设置成 object。

所以 object 的类型是 type,type 继承 object 就是这么来的。


小结



至此,我们算是从解释器的角度完全理清了 Python 中对象之间的关系,用之前的一张图总结一下。

9c34eb9734696c16c0afbba549034f2f.jpg

当然,目前还远远没有结束,后续还会针对内置的对象进行专门的剖析,如浮点数、整数、字符串、字节串、元组、列表、字典、集合等等,都会一点一点剖析。我们会从 Python 的角度介绍对象该怎么用,然后再看它的底层实现,最后再用 Python 代码进行验证,加深理解。

相关文章
|
3月前
ES6中map对象的使用,确实比Object好使哈
ES6中Map对象的使用优势,包括任意类型作为键、直接获取大小、增删查改操作等。Map的键可以是函数、对象、NaN等,支持forEach循环和for...of循环。
32 1
ES6中map对象的使用,确实比Object好使哈
|
2月前
|
JavaScript 前端开发 大数据
在JavaScript中,Object.assign()方法或展开语法(...)来合并对象,Object.freeze()方法来冻结对象,防止对象被修改
在JavaScript中,Object.assign()方法或展开语法(...)来合并对象,Object.freeze()方法来冻结对象,防止对象被修改
28 0
|
4月前
|
Docker 容器
成功解决:Caused by: ParsingException[Failed to parse object: expecting token of type [START_OBJECT] but
这篇文章讨论了在使用Docker启动Elasticsearch容器时遇到的一个具体问题:由于配置文件`elasticsearch.yml`解析出错导致容器启动失败。文章提供了详细的排查过程,包括查看容器的日志信息、检查并修正配置文件中的错误(特别是空格问题),并最终成功重新启动了容器。
|
4月前
|
SQL 存储 数据库
|
4月前
|
UED 开发工具 iOS开发
Uno Platform大揭秘:如何在你的跨平台应用中,巧妙融入第三方库与服务,一键解锁无限可能,让应用功能飙升,用户体验爆棚!
【8月更文挑战第31天】Uno Platform 让开发者能用同一代码库打造 Windows、iOS、Android、macOS 甚至 Web 的多彩应用。本文介绍如何在 Uno Platform 中集成第三方库和服务,如 Mapbox 或 Google Maps 的 .NET SDK,以增强应用功能并提升用户体验。通过 NuGet 安装所需库,并在 XAML 页面中添加相应控件,即可实现地图等功能。尽管 Uno 平台减少了平台差异,但仍需关注版本兼容性和性能问题,确保应用在多平台上表现一致。掌握正确方法,让跨平台应用更出色。
56 0
|
4月前
|
数据采集 API TensorFlow
简化目标检测流程:深入探讨TensorFlow Object Detection API的高效性与易用性及其与传统方法的比较分析
【8月更文挑战第31天】TensorFlow Object Detection API 是一项强大的工具,集成多种先进算法,支持 SSD、Faster R-CNN 等模型架构,并提供预训练模型,简化目标检测的开发流程。用户只需准备数据集并按要求处理,选择预训练模型进行微调训练即可实现目标检测功能。与传统方法相比,该 API 极大地减少了工作量,提供了从数据预处理到结果评估的一站式解决方案,降低了目标检测的技术门槛,使初学者也能快速搭建高性能系统。未来,我们期待看到更多基于此 API 的创新应用。
38 0
|
4月前
【Azure Developer】使用PowerShell Where-Object方法过滤多维ArrayList时候,遇见的诡异问题 -- 当查找结果只有一个对象时,返回结果修改了对象结构,把多维变为一维
【Azure Developer】使用PowerShell Where-Object方法过滤多维ArrayList时候,遇见的诡异问题 -- 当查找结果只有一个对象时,返回结果修改了对象结构,把多维变为一维
|
22天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
76 4
|
2月前
|
Java
Java Object 类详解
在 Java 中,`Object` 类是所有类的根类,每个 Java 类都直接或间接继承自 `Object`。作为所有类的超类,`Object` 定义了若干基本方法,如 `equals`、`hashCode`、`toString` 等,这些方法在所有对象中均可使用。通过重写这些方法,可以实现基于内容的比较、生成有意义的字符串表示以及确保哈希码的一致性。此外,`Object` 还提供了 `clone`、`getClass`、`notify`、`notifyAll` 和 `wait` 等方法,支持对象克隆、反射机制及线程同步。理解和重写这些方法有助于提升 Java 代码的可读性和可维护性。
|
4月前
|
Java
【Java基础面试二十】、介绍一下Object类中的方法
这篇文章介绍了Java中Object类的常用方法,包括`getClass()`、`equals()`、`hashCode()`、`toString()`、`wait()`、`notify()`、`notifyAll()`和`clone()`,并提到了不推荐使用的`finalize()`方法。
【Java基础面试二十】、介绍一下Object类中的方法