🍉 CSDN 叶庭云https://yetingyun.blog.csdn.net/


Python 是一门强大且易用的脚本语言,以其简洁的语法和全面的功能而闻名,能够有效地支持各种业务的快速实现。但 Python 的设计者有意地隐藏了背后的复杂细节。在解决项目问题时,虽然许多问题可能通过搜索引擎找到答案,但由于 Python 的迭代速度非常快,搜索引擎和专业书籍往往无法提供最新和准确的答案。因此,深入了解 Python 的底层架构和核心原理,可以帮助我们更好地理解 Python 的使用方式,从而提高编程技能和调试能力。

在这里插入图片描述

Python 解释器,作为 Python 语言的核心组件,负责将 Python 代码转换为机器码以供计算机执行。Python 解释器有多个主要实现版本,其中,CPython 的使用最为广泛。CPython,作为 Python 的官方解释器,是用 C 语言编写的,并在 Python 社区得到了广泛支持和贡献。除 CPython 外,还有其他解释器实现,如 Jython(Java 实现)、IronPython(.NET 和 Mono 框架实现)和 PyPy(Python 实现,使用了即时编译技术)。尽管它们使用了不同的底层语言,但都保留了 Python 的语法和语义。

Python 解释器是 Python 底层实现的关键部分,通常由 C 语言编写。由于 C 语言具有高效、可移植的特性以及强大的底层操作能力,因此它是 Python 解释器的理想选择。PyPy 之所以比 CPython 更快,主要有以下两个原因:

  • 即时编译(JIT):PyPy 使用了即时编译(Just-In-Time Compilation)技术。这种技术允许 PyPy 在执行程序前先将部分代码编译成机器码,而不是像 CPython 那样逐行解释执行。这种方法结合了提前编译和解释的优点,既提高了性能,又保持了解释型语言的灵活性和跨平台可用性。

  • 运行时优化:PyPy 在实现 Python 时采用了更多的运行时优化,例如更优化的对象布局、更优化的虚函数表查找等。这些优化使得 PyPy 在执行 Python 代码时更加高效。

Python 的标准库使用多种语言实现不同的功能模块,以提供丰富高效的编程体验。Python 的标准库由一系列内置模块组成,这些模块提供了丰富的功能和工具,如文件操作(os 和 pathlib)、网络通信(socket)和数学计算(math)等。标准库中的各个模块是用不同的编程语言实现的,具体使用哪种语言取决于模块和功能的具体需求。标准库中的大部分模块的底层实现都是用 C 语言编写的。C 语言的高效性和强大的底层操作能力使其成为编写性能关键代码的理想选择。例如,math 模块提供了对 C 标准定义的数学函数的访问、os 模块提供对文件和目录操作、socket 模块中的网络编程等,都是用 C 语言实现的,以实现更高的执行效率和更好的性能。

另外,Python 标准库中也包含了一些用其他语言实现的模块。例如,ctypes 模块用于调用 C 语言库函数,它利用 Python 自身的功能与底层 C 代码进行交互。此外,还有一些模块是用 Python 语言实现的,这些模块通常负责一些简单的功能,无需底层语言处理底层细节。

在 Python 中,所有的元素(如整数、浮点数、字符串、列表、元组、字典和集合等)都被视为对象。每个对象都包含一些共享的信息,也就是所谓的 “头部信息”,这些信息存储在 PyObject 结构体中。PyObject 是 Python 对象机制的核心,它是 CPython 对象构造器的基础

PyObject 的定义包含两个主要部分:一个引用计数器和一个对象类型指针。因此,Python 中的每个对象都具有这两个属性:引用计数器和对象类型。对于变长对象,Python 的底层设计提供了一个专门的结构体,这是因为在 Python 中,许多对象都是变长的。以列表(PyListObject 实例)为例,其中的 ob_size 用于维护列表的元素个数。每当插入一个元素,ob_size 就会增加 1;每当删除一个元素,ob_size 就会减少 1。因此,使用 len 函数获取列表的元素个数是一个时间复杂度为 O(1) 的操作,这是因为 ob_size 始终与列表内部的元素个数保持一致,当我们使用 len 函数获取元素个数时,实际上是直接访问了 ob_size。对象与其类型对象的关联是通过对象内部的 PyObject 中的 ob_type,即类型指针来实现的。我们可以通过访问对象的 ob_type 成员来获取类型对象的指针,然后通过这个指针来获取存储在类型对象中的元信息。

Python 的底层实现利用了 PyObject 和 PyTypeObject,从而实现了 C++ 对象的多态特性。在 Python 中,创建对象时会分配内存并进行初始化,然后用 PyObject * 来保存和维护这个对象。因此,在 Python 中,无论是变量的传递还是函数的参数传递,实际上都是传递的一个泛型指针:PyObject *。我们无法直接知道这个指针具体指向什么类型的对象,只能通过其内部的 ob_type 成员进行动态判断。正是因为有了 ob_type,Python 才能实现多态机制。

a + b 这样的变量为例,a 和 b 可以指向各种类型的对象,如整数、浮点数、字符串、列表、元组,甚至是我们自定义并实现了 add 方法的类的实例。在 Python 中,所有的变量都被视为 PyObject * ,因此它们可以指向任何类型的对象。这就导致 Python 无法进行基于类型的优化。Python 首先需要通过 ob_type 来判断变量指向的对象的类型,这在 C 语言中至少需要进行一次属性查找。接着,Python 将每个操作抽象为一个魔法方法。因此,当实例进行加法操作时,需要在类型对象中找到该方法对应的函数指针,这又需要进行一次属性查找。找到函数指针后,将 a 和 b 作为参数传入。这会触发一次函数调用,取出对象的值进行运算,然后根据运算结果创建一个新的对象,并返回其对应的 PyObject * 指针。对于 C 语言,由于类型已经预先定义,a + b 在编译后就变成了一条简单的机器指令。因此,Python 和 C 语言在效率上存在显著差异。

事实上,Python 内部创建对象有两种方法:

  • Python 可以通过泛型或特型的 Python / {/} /C API 创建内置类型的对象。

  • Python 还可以通过对应的类型对象创建,这种方式主要用于自定义类型。Python 提供了 C APIs,允许用户在 C 环境中与其交互。由于 Python 解释器是用 C 语言编写的,因此 Python 内部也大量使用了这些 C APIs。

  • 无论采用哪种方式创建对象,最终的关键步骤都是分配内存。对于内置类型的实例对象,Python 可以直接分配内存。这是因为这些类型的成员在底层都是固定的,Python 对此非常了解,因此可以通过 Python / {/} /C API 直接分配内存并进行初始化。

比如创建列表:可以使用 list()、也可以使用 [ ];创建元组:可以使用 tuple()、也可以使用 ();创建字典:可以使用 dict()、也可以使用 {}。前者是通过类型对象去创建的,后者是通过 Python / {/} /C API 创建。但对于内置类型,我们推荐使用 Python / {/} /C API 进行创建,它会直接解析为对应的 C 一级数据结构。这些结构在底层已经实现好,可以直接使用,无需通过如 list() 这种调用类型对象的方式来创建。

通过 PyObject 的循环双向链表,CPython 可以很方便地管理底层真正分配内存的变量。我们定义的 a、b、c 都是在栈上创建的变量,它们实际上都是对堆上对象的引用,因此只存储了堆对象在堆上的位置。

Python 的内存管理为开发人员提供了便利,自动处理了许多底层细节,减少了内存泄漏,提高了程序性能。在 Python 中,通过引用计数来跟踪每个对象的使用情况,当对象的引用计数降至零时,垃圾回收器会自动释放该对象的内存。

含有垃圾回收机制的编程语言,几乎都会采用引用计数来管理类,这种方法可以解决超过 80% 的自动回收问题。引用计数的基本思想很简单:每个对象在创建时,其引用计数器 refcnt 就被初始化为 1。每当有新的引用指向它时,refcnt 就增加 1;每当减少一个引用,refcnt 就减少 1。当一个对象的 refcnt 降为 0 时,意味着没有任何变量再引用这个对象,此时,解释器会销毁这个对象,并释放其占用的内存。我们可以通过 Python 的内置库 sys 中的 getrefcount 函数,来获取当前变量引用的底层对象的引用次数。但是,需要注意的是,由于在调用 getrefcount 函数时,传入的对象在函数内部被引用了一次,因此,实际的引用次数应该是 sys.getrefcount(a) 的结果减 1。因此,通过这种方式,CPython 能够高效地管理我们创建的对象,并自动释放不再使用的对象。

仅依赖引用计数器进行对象的自动销毁存在问题,因为它无法处理容器对象的循环引用(例如列表)。为了避免循环引用,系统并不直接根据 refcnt 的值来释放对象,而是周期性地采用标记清除算法进行自动销毁。标记清除算法将栈变量和堆变量一起建模为一个有向图。在删除了部分栈空间中的变量后,解释器会遍历栈变量,并从每个栈变量(也称为 root_object)开始遍历整个有向图。被遍历到的节点会被标记为可达。当所有栈变量都被遍历完后,未被标记为可达的节点对应的堆变量将被销毁并释放。

实际上,CPython 的标记清除算法需要遍历整个栈空间和大部分堆空间,当变量数量较多时,这将非常耗时。因此,我们不能在链表每次变动后就立即执行一次标记清除算法。那么,我们应该多久执行一次标记清除算法呢?为此,CPython 引入了分代回收机制来规定标记清除算法的执行周期,并对这个过程进行了一些优化。

此外,变量缓存是 Python 底层变量管理的一种优化手段,主要体现在内存申请和管理上。


📚️ 参考链接:

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐