Python源码剖析:深度探索Cpython对象-达观数据

news2024/11/24 4:02:42
CPython 是 Python 社区的标准,其他版本的 Python,比如 pypy,都会遵行 CPython 的标准 API 实现。想要更深入的认识 Python,就需要了解 CPython 的源码实现。本文将从 CPython 的对象构造器开始入手,带大家揭开 CPython 源码的面纱,带你进入 C + Python 的世界。文章的最后,你也会对 Python 中最重要的概念:一切皆对象 (Object) 有更深刻的认识;你还会发现一些具体的线索,为什么 Python 用起来比其他静态类型语言慢很多。

一、为什么要学习 Python 源码

Python 是一门上层语言,创建者通过有意设计来隐藏背后复杂的细节 (builtins)。在解决项目问题时,很多问题也许能通过搜索引擎找到答案,但 Python 是一门迭代速度非常快的语言,搜索引擎与专业书难以获得实效性好且准确的答案,因此多了解其架构与核心原理,可以更好地理解Python语言的使用方式、提高编程技能和调试能力。

二、CPython 整体架构

CPython 整体架构大致分为三个模块:

  1. 代码文件 File Groups - Python 所提供的的大量的模块、库、以及用户自定义的模块。用户还可以通过自定义模块来扩展 Python 系统。

  2. 解释器 Python Core - 又称 Python 虚拟机,对代码分析理解,翻译成字节流,并运行这些字节代码。

    · Scanner 负责词法分析的工作,将代码一行一行切分为 Token

    · Parser 则负责语法分析,将 Token 组织为抽象语法树

    · Compiler 则将语法树转化为指令集合的字节码流

    · Code Evaluator 也是我们常说 Python 虚拟机,负责执行这些字节码

  3. 运行环境 Runtime Env - 包括运行时的对象、基础类型结构、内存分配器和实时的运行状态信息。

    · Object 和 Type Structure 分别是程序在运行过程中生成的对象和Python中的自带内建对象,如 Int、Str、List 等

    · Memory Allocator 则负责申请创建对象需要的内存,本质就是封装了 C 语言里面的 malloc() 函数

    · Current State 负责维护运行时的各类状态信息,以便在程序执行过程中如果发生状态变化(正常态和异常态)时,仍然能正常运行

三、编译 CPython

我们可以从下文的 GitHub 地址下载各版本的 CPython 源代码(本文内容以 Python 3.11 为例),其目录结构如下:

接下来,我们将从源代码编译 CPython。此步骤需要 C 编译器和一些构建工具。不同的系统编译方法也不同,这里我用的是 mac 系统。

在上述命令中,你需要下载并安装一些工具,包括 Homebrew,Git,Make, GNU C 编译器和OpenSSL等。./configure步骤用来自动化构建过程,CPPFLAGS 是 c 和 c++ 编译器的选项,这里指定了 zlib 头文件的位置,LDFLAGS 是 gcc 等编译器会用到的一些优化参数,这里是指定了 zlib 库文件的位置,(brew --prefix openssl) 显示的是 openssl 的安装路径,运行完上面命令以后在存储库的根目录中会生成一个 Makefile,你可以通过运行以下命令来构建 CPython 二进制文件。make -j2-j2 标志允许 make 同时运行 2 个作业来加快编译速度。在构建期间,你可能会收到一些错误,例如,dbm,sqlite3,uuid,nis,ossaudiodev,spwd 和tkinter 将无法使用这组指令构建。如果你不打算针对这些软件包进行开发,这些错误没什么影响。构建将花费几分钟并生成一个名为 python.exe 的二进制文件,虽然它的后缀是 exe 格式,但它确实是 macOS 下的可执行文件。每次改动源代码,都需要重新运行 make 进行编译。

四、了解 Python 对象

(一)PyObject 和 PyVarObject

Python 中一切皆对象,而所有的对象都拥有一些共同的信息(也叫头部信息),这些信息就在 PyObject 中,PyObject 是 Python 整个对象机制的核心,是 CPython 对象构造器的基石,我们来看看它的定义:

因此我们看到 PyObject 的定义非常简单,就是一个引用计数和一个类型指针,所以 Python 中的任意对象都必有引用计数和类型这两个属性。针对变长对象,Python 底层也提供了一个结构体,因为 Python 里面很多都是变长对象。我们来看看 PyVarObject 的定义:

例如列表(PyListObject 实例)中的 ob_size 维护的就是列表的元素个数,插入一个元素,ob_size 会加1,删除一个元素,ob_size 会减1。因此,我们使用 len 获取列表的元素个数是一个时间复杂度为 O(1) 的操作,因为 ob_size 始终和内部的元素个数保持一致,使用 len 获取元素个数的时候会直接访问 ob_size。

(二)PyTypeObject 类型对象

而将一个对象和其类型对象关联起来的,毫无疑问正是该对象内部的 PyObject 中的 ob_type,也就是类型指针。我们通过对象的 ob_type 成员即可获取类型对象的指针,通过该指针可以获取存储在类型对象中的某些元信息。我们来看看 _typeobject 的几个关键的成员:

事实上从名字上你也能看出来这每一个成员代表的含义,与我们在 Python 中常用的魔法方法很像。而且这里面的成员虽然多,但并非每一个类型对象都具备,比如 int 类型它就没有 tp_as_sequence 和 tp_as_mapping,所以 int 类型的这两个成员的值都是 0。综上所述,Python 底层通过 PyObject 和 PyTypeObject 完成了 C++ 所提供的对象的多态特性。在 Python 中创建一个对象,会分配内存并进行初始化,然后 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 在编译之后就是一条简单的机器指令,因此两者在效率上差别很大。

(三)对象的创建与调用

抛出个问题: item = 2.71 和 item = float(2.71) 得到的结果都是2.71,但它们之间有什么不同呢。或者说列表: lst = [] 和 lst = list()得到的 lst 也都是一个空列表,但这两种方式有什么区别呢?Python 中有许多效果相同,过程不同的表达,值得我们进一步思考。

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

• 通过 Python/C API,可以是泛型API、也可以是特型API,用于内置类型

• 通过对应的类型对象去创建,多用于自定义类型Python 对外提供了 C API,让用户可以从 C 环境中与其交互。由于 Python 解释器是用 C 写成的,所以 Python 内部也在大量使用这些 C API。为了更好的研读源码,系统地了解这些 API 的组成结构是很有必要的,下面以 PyFloatObject 对象为例,通过源码的大致步骤了解它的两种创建过程。首先先看浮点数的定义:

可以看出,PyFloatObject 的结构非常简单,除了 PyObject 这个公共的头部信息之外,只有一个额外的 ob_fval,用于存储具体的值,并且使用的是 C 中的 double。以 f = 3.14 为例,底层结构如下:

使用泛型 API 创建


 

使用特型 API 创建

综上,不管采用哪种方式创建,最终的关键步骤都是分配内存,创建内置类型的实例对象,Python 是可以直接分配内存的。因为它们有哪些成员在底层都是写死的,Python 对它们了如指掌,因此可以通过 Python/C API 直接分配内存并初始化。以 PyFloat_FromDouble 为例,直接在接口内部为 PyFloatObject 结构体实例分配内存,并初始化相关字段即可。从下文的实验也可以看出,对于内置类型的实例对象而言,使用 Python / C API 创建要快不少。

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

  五、总结     

Python是一门备受推崇的脚本语言,以其简单的语法和全面的功能而著称,可快速实现各种业务。本文从 CPython 对象构造器入手,介绍了浮点数对象在 CPython 底层数据结构中的表现形式以及对象创建的过程。通过进一步了解 CPython 动态性的实现方式,读者可望在阅读 CPython 源码后提升编写高质量代码的能力。参考资料:

  1. https://github.com/python/cpython
  2. https://docs.python.org/zh-cn/3.11/c-api/index.html
  3. https://jiuaidu.com/jianzhan/990904/
  4. https://www.ab62.cn/article/15965.html
  5. https://zhuanlan.zhihu.com/p/596637636

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/749548.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

工作有感:莫名的IT培训班生涯

欢迎关注博主 六月暴雪飞梨花 或加入【六月暴雪飞梨花】一起学习和分享Linux、C、C、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,滤波估计、多传感器信息融合,机器学习,人工智能等相关领域的知识和技术…

什么是『知识管理系统』?为什么企业需要搭建知识管理系统?

你的企业或团队在知识迁移的过程中有遇到过以下问题吗? 1、花费过长时间的员工培训:当新员工入职时,传统的培训方法可能需要大量时间和资源。而且,由于知识可能分散在各种文档、手册和资源中,培训过程变得更加复杂。这…

Go Frame 2.3驱动全栈前后端分离管理系统的创新突破

基于新的Go Frame 2.3框架开发的全栈前后端分离的管理系统——GFast-V3,正以其创新性和突破性的特点引起了广泛关注。该系统通过将前端与后端解耦,实现了更高效、可扩展的开发模式,为企业和开发者带来了新的开发体验。 GFast-V3系统介绍 gfa…

重磅预告!Apache DolphinScheduler 3.2.0 新功能“剧透”

近期,Apache DolphinScheduler 将迎来 3.2.0 版本的到来。本次发版为大版本发布,将会带来众多大家期待已久的新功能和新改进。为了让用户提前感知到新版本的变化,社区特意提前“剧透”新版本的重要 feature,并制作了视频进行介绍。…

unity的CommandBuffer介绍

大家好,我是阿赵。   之前介绍过使用PostProcessing来做屏幕后处理效果。我们不一定要用PostProcessing来做后处理效果。   PostProcessing功能强大,比如不同的layer控制不同的屏幕效果,比如可以使用PostProcessVolume的非全局效果达到某…

【三维重建】【深度学习】NeuS总览

【三维重建】【深度学习】NeuS总览 论文提出了一种新颖的神经表面重建方法,称为NeuS,用于从2D图像输入以高保真度重建对象和场景。在NeuS中建议将曲面表示为有符号距离函数(SDF)的零级集,并开发一种新的体绘制方法来训练神经SDF表示&#xff…

【Distributed】分布式ELK日志文件分析系统(一)

文章目录 一、ELK 概述1. 为什么要使用 ELK2. 完整日志系统基本特征3. ELK 简介3.1 ElasticSearch(ES)3.2 Kiabana3.3 Logstash3.4 其它组件Filebeat缓存/消息队列Fluentd 4. ELK 的工作原理5. Linux 系统内核日志消息的优先级别 二、 部署 ELK 集群服务…

二叉树题目:合并二叉树

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法一思路和算法代码复杂度分析 解法二思路和算法代码复杂度分析 题目 标题和出处 标题:合并二叉树 出处:617. 合并二叉树 难度 3 级 题目描述 要求 给定两个二叉树 root1 \texttt{root1…

java项目之多人命题系统(ssm+mysql+jsp)

风定落花生,歌声逐流水,大家好我是风歌,混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的多人命题系统。技术交流和部署相关看文章末尾! 开发环境: 后端: 开发语言:Java 框架&…

zabbix部署及应用(自定义监控内容,zabbix 自动发现与自动注册,zabbix 代理服务器,Zabbix 高可用集群)

zabbix部署及应用 一、添加 zabbix 客户端主机1、服务端和客户端都配置时间同步2、服务端和客户端都设置 hosts 解析3、设置 zabbix 的下载源,安装 zabbix-agent24、修改 agent2 配置文件5、启动 zabbix-agent26、在服务端验证 zabbix-agent2 的连通性7、常用的键值…

一、对象的概念(1)

本章概要 抽象接口服务提供封装 抽象 “我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们。语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” – Alfred Korzybski (1930) 计算机革命的起…

分布式锁【 基于synchronized锁解决超卖问题、分布式锁解决方案、悲观锁实现的分布式锁】(二)-全面详解(学习总结---从入门到深化)

目录 分布式锁问题_演示问题 基于synchronized锁解决超卖问题 分布式锁解决方案 分布式锁实现方案 分布式锁解决方案_数据库悲观锁实现的分布式锁 项目中使用for update 分布式锁问题_演示问题 启动订单服务9090 启动订单服务9091 创建两个SpringBoot服务 启动Nginx服务 下载N…

火山引擎云搜索服务升级云原生新架构;提供数十亿级分布式向量数据库能力

从互联网发展伊始,搜索技术就绽放出了惊人的社会和经济价值。随着信息社会快速发展,数据呈爆炸式增长,搜索技术通过数据收集与处理,满足信息共享与快速检索的需求。 云搜索服务 ESCloud 是火山引擎提供的完全托管在线分布式搜索服…

OJ练习第135题——下降路径最小和

下降路径最小和 力扣链接:931. 下降路径最小和 题目描述 给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。 下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择…

永磁同步电机磁场定向控制(FOC)及Matlab/Simulink仿真分析

前言 做永磁同步电机控制绕不开FOC,本章节主要介绍FOC控制的基本原理、坐标变换以及永磁同步电机在同步旋转坐标系下的数学模型,并通过Matlab/Simulink进行永磁同步电机FOC控制算法的仿真分析。 01 FOC的基本原理 磁场定向控制(Field-Ori…

分割3-转置卷积可逆吗?

上一话 分割2——语义分割评价指标https://blog.csdn.net/XiaoyYidiaodiao/article/details/125149509?spm1001.2014.3001.5502 转置卷积不是卷积的逆操作,并且转置卷积是卷积操作! 这是因为普通卷积的操作图 1. 图1 其卷积可等效为图 2. 图2 其卷积操…

Offset Explorer2 监视kafka的利器

kafka作为一个生产者和消费者集为一体的框架,消费者必须一直保持打开的状态,并且每隔一段时间接收一次数据,才能够保持生产者放入的数据及时被处理掉,而生产者则可以每隔一段时间发送一波数据,这样消费者就能够接收到了…

Mysql搭建互为主从数据库

Mysql搭建互为主从数据库 一、搭建前期说明二、mysql文件结构以及配置说明1、mysqlA文件结构2、mysqlB文件结构3、mysqlA的配置文件my.cnf4、mysqlB的配置文件my.cnf5、启动mysqlA服务器脚本startMysqlA.sh6、启动mysqlB服务器脚本startMysqlB.sh7、查看服务启动情况 三、设置主…

【Redis】之缓存一致性

1、缓存一致性 对于使用 Redis 作为缓存来说,如何保证数据库和缓存数据一致性是个麻烦的问题。对于缓存和数据库的操作,主要有以下两种方式: 先删缓存,再更新数据库;先更新数据库,再删除缓存;…

【UE4 塔防游戏系列】02-基础设置

步骤 1. 新建一个蓝图,父类为游戏模式基础 命名为“TaFangGameMode” 2. 新建一个玩家控制器 命名为“TaFangGamePlayerController” 3. 在世界场景设置中,选择游戏覆盖模式为“TaFangGameMode” 默认Pawn类设为None,玩家控制器类选择“TaFa…