编译原理:python编译器--运行时机制
python的运行时机制的核心 -- python对象机制的设计
理解字节码的执行过程
用 GDB 跟踪执行一个简单的示例程序,它只有一行:“a=1”。
对应的字节码如下。其中,前两行指令实现了"a = 1"的功能(后两行是根据Python的规定,在执行完一个模块之后,缺省返回一个None值)
PS: _PyEval_EvalFrameDefault() 函数这里设置一个断点
首先是执行第一行指令,LOAD_CONST
编译器会做三件事:
1.从常数表里取出0号常数。编译完毕以后会形成 PyCodeObject,而在这个对象里会记录所有的常量、符号名称、本地变量等信息。常量 1 就是从它的常量表中取出来的。
2.把对象引用值加1。对象引用跟垃圾收集机制相关
3.把这个常数对象入栈。
接下来分析每一条指令,能有什么信息。
第一个条指令:
第一个信息:常量1在python内部,它是个对象,这个对象的类型是PyLong_Type型,这是Python的整型内部实现
另外,该对象的引用数是 126 个,说明这个常量对象其实是被共享的,LOAD_CONST 指令会让它的引用数加 1。我们用的常数是 1,这个值在 Python 内部也是会经常被用到,所以引用数会这么高。
进一步发现,往栈里放的数据,它是个对象指针,而不像 Java 的栈机那样,是放了个整数。
总结上述信息,有个结论:在Python里,程序中的任何符号都是对象,包括整数,浮点数这些基础数据,或者是自定义的类,或者是函数,他们都是对象。在栈机里处理的都是这些对象的引用
下一条指令:STORE_NAME指令
执行STORE_NAME指令时,解释器做了5件事:
1.根据指令的参数,从名称表里取出变量名称。这个名称表也来自于
PyCodeObject
2.从栈顶弹出上一步存进去的常量对象
3.获取保存了所有本地变量的字典,这也来自于
PyCodeObject
4.在字典里,设置a的值为该常量。深入跟踪其执行过程,会发现在存入字典的时候,name 对象和 v 对象的引用都会加 1。因为它们一个作为 key,一个作为 value,都要被字典所引用。
5.减少常量对象的引用技术,意思就是栈机本身不再引用该常量。
Python对象的设计
Python的对象定义在object.h
中。对象是分配在堆上,对象不能静态分配或分配在栈上,他们只能通过特殊的宏和函数来访问。一个对象(object)通过引用计数的方式来决定该对象是否需要释放。
一个对象可以通过PyObject *
类型的指针访问。PyObject *
是一个只包含了引用计数和类型指针的结构体
typedef struct _object { //定长对象
Py_ssize_t ob_refcnt; //对象引用计数
struct _typeobject *ob_type; //对象类型
} PyObject;
typedef struct { //变长对象
PyObject ob_base;
Py_ssize_t ob_size; //变长部分的项目数量,在申请内存时有确定的值,不再变
} PyVarObject;
PyObject 是最基础的结构,所有的对象在 Python 内部都表示为一个 PyObject 指针。它里面只包含两个成员:对象引用计数(ob_refcnt)和对象类型(ob_type)
可能会问:为什么只有两个成员,对象的数据(比如一个整数)保存在哪了?
其实,任何对象都会在一开头包含 PyObject,其他数据都跟在 PyObject 的后面。比如说,Python3 的整数的设计是一个变长对象,会用一到多个 32 位的段,来表示任意位数的整数:
#define PyObject_VAR_HEAD PyVarObject ob_base;
struct _longobject {
PyObject_VAR_HEAD //PyVarObject
digit ob_digit[1]; //数字段的第一个元素
};
typedef struct _longobject PyLongObject; //整型
它在内存中的布局是这样的:
PyObject、PyVarObject和PyLongObject指向的内存地址是相同的。你可以根据 ob_type 的类型,把PyObject强制转换成PyLongObject*。
用sys.getsizeof()
函数,来测量对象占据的内存空间。
>>> a = 10
>>> import sys
>>> sys.getsizeof(a)
28 #ob_size = 1
>>> a = 1234567890
>>> sys.getsizeof(a)
32 #ob_size = 2,支持更大的整数
总结下Python对象设计的三个特点:
1.基于堆
Python 对象全部都是在堆里申请的,没有静态申请和在栈里申请的。这跟 C、C++ 和 Java 这样的静态类型的语言很不一样。2.基于引用计数的垃圾收集机制
每个 Python 对象会保存一个引用计数。也就是说,Python 的垃圾收集机制是基于引用计数的。
优点是可以实现增量收集,只要引用计数为零就回收,避免出现周期性的暂停;
缺点是需要解决循环引用问题,并且要经常修改引用计数(比如在每次赋值和变量超出作用域的时候),开销有点大。
- 3.唯一ID
每个 Python 对象都有一个唯一 ID,它们在生存期内是不变的。用id()
函数就可以获得对象的 ID。
接下来,我们看看 ob_type
这个字段,它指向的是对象的类型。以这个字段为线索,我们就可以牵出 Python 的整个类型系统的设计。
Python的类型系统
Python是动态类型的语言。它的类型系统的设计相当精巧,Python语言的很多优点,都来自于它的类型系统。现在来看下它的类型系统
首先,Python里每个PyObject对象都有一个类型信息。保存类型信息的结构体是PyTypeObject(定义在include/cpython/object.h中)。PyTypeObject 本身也是一个 PyObject,只不过这个对象是用于记录类型信息的而已。它是一个挺大的结构体,包含了对一个类型的各种描述信息,也包含了一些函数的指针,这些函数就是对该类型可以做的操作。可以说,只要你对这个结构体的每个字段的作用都了解清楚了,那么你对 Python 的类型体系也就了解透彻了。
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* 用于打印的名称格式是"<模块>.<名称>" */
Py_ssize_t tp_basicsize, tp_itemsize; /* 用于申请内存 */
/* 后面还有很多字段,比如用于支持数值计算、序列、映射等操作的函数,用于描述属性、子类型、文档等内容的字段,等等。 */
...
} PyTypeObject
因为这个结构非常重要,有代表性的字段整理如下:
这个结构里的很多部分都有一个函数插槽(Slot),可以往插槽里保存一些函数指针,用来实现各种标准操作,比如对象生命周期管理/转成字符串/获取哈希值等。
像__init__
这样的方法,它的两边都是有两个下划线的,也就是"double underscore",简称dunder方法,也叫做"魔术方法"。 在用 Python 编写自定义的类的时候,你可以实现这些魔术方法,它们就会被缺省的tp_*函数所调用,比如,“init”会被缺省的tp_init函数调用,完成类的初始化工作。
现在以整型对象为例,看下PyTypeObject长什么样子。
对于整型对象来说,它的ob_type
会指向一个PyLong_Type
对象(这个对象在longobject.c中初始化的,它是PyTypeObject的一个实例)
看到一些信息:类型名称是“int”,转字符串的函数是long_to_decimal_string,此外还有比较函数、方法描述、属性描述、构建和析构函数等。
运行type()
函数,可以获得一个对象的类型名称,这个名称就来自 PyTypeObject
的tp_name
。
>>> a = 10
>>> type(a)
<type 'int'>
用 dir() 函数
,可以从 PyTypeObject 中查询出一个对象所支持的所有属性和方法。比如,下面是查询一个整型对象获得的结果:
整型对应的 PyTypeObject 的实例是 PyLong_Type。Python 里其实还有其他一些内置的类型,它们分别都对应了一个 PyTypeObject 的实例。你可以参考一下这个表格。
Python 对象的一些协议
在研究整型对象的时候,发现 PyLong_Type 的 tp_as_number 字段被赋值了,这是一个结构体(PyNumberMethods),里面是很多与数值计算有关的函数指针,包括加减乘除等。这些函数指针是实现 Python 的数值计算方面的协议。任何类型,只要提供了这些函数,就可以像整型对象一样进行计算。这实际上是 Python 定义的一个针对数值计算的协议。
也可以使用GDB跟踪下python的执行过程,卡那卡那富整数的加法是如何实现的。
a = 1
b = a + 2
它对应的字节码如下:
重点来关注 BINARY_ADD 指令的执行情况,如下图所示:
如果 + 号两边是字符串,那么编译器就会执行字符串连接操作。否则,就作为数字相加。
继续跟踪进入 PyNumber_Add 函数。在这个函数中,Python 求出了加法函数指针在 PyNumberMethods 结构体中的偏移量,接着就进入了 binary_op1() 函数。
在 binary_op1 函数中,Python 首先从第一个参数的类型对象中,取出了加法函数的指针。你在 GDB 中打印出输出信息,就会发现它是 binaryfunc 类型的,函数名称是 long_add。
binaryfunc 类型的定义是:
typedef PyObject * (*binaryfunc)(PyObject *, PyObject *);
也就是说,它是指向的函数要求有两个 Python 对象(的指针)作为参数,返回值也是一个 Python 对象(的指针)。
再继续跟踪下去,会发现程序就进入到了 long_add
函数。这个函数是在 longobject.c 里
定义的,是 Python 整型类型做加法计算的内置函数。
除了内置的函数,我们也可以自己写这样的函数,并被 Python 所调用。来看看下面的示例程序,我们定义了一个“add”魔术方法。这个方法会被 Python 作为 SimpleComplex 的加法函数所使用,实现了加法操作符的重载,从而支持复数的加法操作。
class SimpleComplex(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return "x: %d, y: %d" % (self.x, self.y)
def __add__(self, other):
return SimpleComplex(self.x + other.x, self.y + other.y)
a = SimpleComplex(1, 2)
b = SimpleComplex(3, 4)
c = a + b
print(c)
那么对于这么一个自定义类,在执行 BINARY_ADD 指令时会有什么不同呢?
首先,在 SimpleComplex 的 type 对象中,add 函数插槽里放了一个 slot_nb_add() 函数指针,这个函数会到对象里查找“add”函数。因为 Python 的一切都是对象,因此它找到的是一个函数对象。
所以,接下来,Python 需要运行这个函数对象,而不是用 C 语言写的内置函数。那么怎么运行这个函数对象呢?
这就需要用到 Python 的另一个协议,Callable 协议。这个协议规定,只要为对象的类型中的 tp_call 属性定义了一个合法的函数,那么该对象就是可被调用的。
对于自定义的函数,Python 会设置一个缺省的 tp_call 函数。这个函数所做的事情,实际上就是找到该函数所编译出来的 PyCodeObject,并让解释器执行其中的字节码!
对于自定义的函数,Python 会设置一个缺省的 tp_call 函数。这个函数所做的事情,实际上就是找到该函数所编译出来的 PyCodeObject,并让解释器执行其中的字节码!
Python对象的创建
用 Python 语言,我们可以编写 class,来支持自定义的类型。我们来看一段示例代码:
class myclass:
def __init__(self, x):
self.x = x
def foo(self, b):
c = self.x + b
return c
a = myclass(2);
其中,myclass(2) 是生成了一个 myclass 对象。
Python 创建一个对象实例的方式,其实跟调用一个函数没啥区别(不像 Java 语言,还需要 new 关键字)。如果你不知道 myclass 是一个自定义的类,你会以为只是在调用一个函数而已。
不过,前面已经提到了 Python 的 Callable 协议。所以,利用这个协议,任何对象只要在其类型中定义了 tp_call,那么就都可以被调用。
为加深你对 Callable 协议的理解。在下面的示例程序中,定义了一个类型 Bar,并创建了一个对象 b
class Bar:
def __call__(self):
print("in __call__: ", self)
b = Bar()
b() #这里会打印对象信息,并显示对象地址
在 b 对象后面加一对括号,就可以调用 b 了!实际执行的就是 Bar 的“call”函数(缺省的 tp_call 函数会查找“call”属性,并调用)
所以,前面调用 myclass(),那一定是因为 myclass 的类型对象中定义了 tp_call。
可以把“myclass(2)”这个语句编译成字节码看看,它生成的是 CALL_FUNCTION 指令
,与函数调用没有任何区别。
换句话说,换句话说,一个普通的对象的类型,是一个类型对象。那么一个类型对象的类型又是什么呢?
答案是元类(metaclass),元类是类型的类型。 举例来说,整型的 metaclass 是 PyType_Type。其实,大部分类型的 metaclass 是 PyType_Type。
所以说,调用类型来实例化一个对象,就是调用 PyType_Type
的 tp_call 函数。那么 PyType_Type
的 tp_call
函数都做了些什么事情呢?
这个函数是 type_call()
,它也是在** typeobject.c ** 中定义的。Python 以 type_call()
为入口,会完成创建一个对象的过程:
- 创建
tp_call 会调用类型对象的 tp_new 插槽的函数。对于 PyLong_Type 来说,它是 long_new。
如果我们是创建一个 Point 对象,如果你为它定义了一个“new”函数,那么就将调用这个函数来创建对象,否则,就会查找基类中的 tp_new。
- 初始化
tp_call 会调用类型对象的 tp_init。对于 Point 这样的自定义类型来说,如果定义了“init”函数,就会执行来做初始化。否则,就会调用基类的 tp_init。对于 PyBaseType_Object 来说,这个函数是 object_init。
除了自定义的类型,内置类型的对象也可以用类型名称加括号的方式来创建。我还是以整型为例,创建一个整型对象,也可以用“int(10)”这种格式,其中 int 是类型名称。而且,它的 metaclass 也是 PyType_Type。
当然,你也可以给你的类型指定另一个 metaclass,从而支持不同的对象创建和初始化功能。虽然大部分情况下你不需要这么做,但这种可定制的能力,就为你编写某些特殊的功能(比如元编程)提供了可能性。
类型的类型是元类(metaclass),它能为类型的调用提供支持。你可能进一步会问,那么元类的类型又是什么呢?是否还有元元类?直接调用元类又会发生什么呢?
缺省情况下,PyType_Type 的类型仍然是 PyType_Type,也就是指向它自身。 对元类做调用,也一样会启动上面的 tp_call() 过程。
到目前为止,我们谈论 Python 中的对象,还没有谈论那些面向对象的传统话题:继承啦、多态啦,等等。这些特性在 Python 中的实现,仍然只是在类型对象里设置一些字段即可。你可以在 tp_base 里设定基类(父类)来支持继承,甚至在 tp_bases 中设置多个基类来支持多重继承。所有对象缺省的基类是 object,tp_base 指向的是一个 PyBaseObject_Type 对象。
>>> int.__base__ #查看int类型的基类
<class 'object'>
到目前为止,我们已经对于对象的类型、元类,以及对象之间的继承关系有了比较全面的了解,为了方便你重点复习和回顾,我把它们画成了一张图。
Python 对象的类型关系和继承关系:
图中我用两种颜色的箭头区分了两种关系。一种是橙色箭头,代表的是类型关系, 比如 PyLong_Type 是 PyLongObject 的类型,而 PyType_Type 是 PyLong_Type 的类型;另一种是黑色箭头,代表的是继承关系,比如 int 的基类是 object,所以 PyLong_Type 的 tp_base 指向 PyBaseObject_Type
小结
Python 的运行时设计的核心,就是 PyObject 对象,Python 对象所有的特性都是从 PyObject 的设计中延伸出来的,给人一种精巧的美感。
Python 程序中的符号都是 Python 对象,栈机中存的也都是 Python 对象指针。
所有对象的头部信息是相同的,而后面的信息可扩展。这就让 Python 可以用 PyObject 指针来访问各种对象,这种设计技巧你需要掌握。
每个对象都有类型,类型描述信息在一个类型对象里。系统内有内置的类型对象,你也可以通过 C 语言或 Python 语言创建新的类型对象,从而支持新的类型。
类型对象里有一些字段保存了一些函数指针,用于完成数值计算、比较等功能。这是 Python 指定的接口协议,符合这些协议的程序可以被无缝集成到 Python 语言的框架中,比如支持加减乘除运算。
函数的运行、对象的创建,都源于 Python 的 Callable 协议,也就是在类型对象中制定 tp_call 函数。面向对象的特性,也是通过在类型对象里建立与基类的链接而实现的。
编译原理:python编译器--运行时机制的更多相关文章
- Stack overflow 编译能通过,运行时出现Stack overflow
Stack overflow 编译能通过,运行时出现Stack overflow 大家都知道,Windows程序的内存机制大概是这样的,全局变量(局部的静态变量本质也属于此范围)存储于堆内存,该段内存 ...
- iOS开发之runtime运行时机制
最近参加三次面试都有被问到runtime,因为不太懂runtime我就只能支支吾吾的说点零碎.我真的好几次努力想看一看runtime的知识,因为知道理解它对理解OC代码内部变化有一定帮助,不过真心觉得 ...
- runtime 运行时机制 完全解读
runtime 运行时机制 完全解读 目录[-] import import 我们前面已经讲过一篇runtime 原理,现在这篇文章主要介绍的是runtime是什么以及怎么用!希望对读者有所帮助! ...
- Runtime运行时机制
Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 OC 代码,底层都是基于它来实现的 我们需要了解的是 Objective-C 是一门动态语言, ...
- 浅谈C++编译原理 ------ C++编译器与链接器工作原理
原文:https://blog.csdn.net/zyh821351004/article/details/46425823 第一篇: 首先是预编译,这一步可以粗略的认为只做了一件事情,那就 ...
- iOS-浅谈runtime运行时机制-runtime简单使用(转)
转自http://www.cnblogs.com/guoxiao/p/3583432.html 由于OC是运行时语言,只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法.利用runtim ...
- iOS-浅谈runtime运行时机制02-runtime简单使用
http://blog.csdn.net/jiajiayouba/article/details/44201079 由于OC是运行时语言,只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方 ...
- Python 函数运行时更新
Python 动态修改(运行时更新) 特性 实现函数运行时动态修改(开发的时候,非线上) 支持协程(tornado等) 兼容 python2, python3 安装 pip install realt ...
- android apk 防止反编译技术第二篇-运行时修改字节码
上一篇我们讲了apk防止反编译技术中的加壳技术,如果有不明白的可以查看我的上一篇博客http://my.oschina.net/u/2323218/blog/393372.接下来我们将介绍另一种防止a ...
- android apk 防止反编译技术第二篇-运行时修改Dalvik指令
上一篇我们讲了apk防止反编译技术中的加壳技术,如果有不明白的可以查看我的上一篇博客http://my.oschina.net/u/2323218/blog/393372.接下来我们将介绍另一种防止a ...
随机推荐
- Deepseek学习随笔(2)--- 快速上手DeepSeek
注册与登录 要开始使用 DeepSeek,你需要先注册一个账号.以下是具体步骤: 访问 DeepSeek 官网. 使用邮箱或手机号注册账号. 登录后进入控制台,开始使用. 控制台功能介绍 DeepSe ...
- Azure - [01] 订阅管理
题记部分 001 || 核心功能 (1)访问控制 Azure订阅通过基于角色的访问控制(RBAC)系统,允许管理员精细管理用户.组和应用程序对资源的访问权限.RBAC系统通过将权限分配给角色,再将 ...
- python - [12] 脚本一文通
题记部分 一.文件夹&文件 (1)删除空文件夹 # 删除目录中的空文件夹 import os def move_epty_folders(directory_path): for root, ...
- AI回答:php中间件
在PHP中,中间件(Middleware)是一种用于在处理请求和生成响应之间插入额外逻辑的机制.中间件通常用于执行诸如身份验证.日志记录.缓存.错误处理等任务.PHP本身并没有内置的中间件系统,但许多 ...
- 详解nginx配置url重定向-反向代理
https://www.jb51.net/article/99996.htm 本文系统:Centos6.5_x64 三台主机:nginx主机,hostname: master.lansgg.com ...
- 写一个简单的SQL生成工具
知识点: MyBatis 语法概览 MyBatis 是一个强大的数据持久化框架,它提供了一种半自动化的 ORM 实现方式.通过 MyBatis,开发者可以通过简单的 XML 或注解来配置和映射原生信息 ...
- 【记录】C/C++-关于I/O的坑与教训
吐槽 每每读取字符串时,倘若稍有灵活的操作,总会遇上诡异奇怪的事情.究其原因,就是没完全理解一些基本读写函数的机制.这次做Uva227就把I/O上的问题全暴露出来了.想来还是应该记录一些经验教训. 记 ...
- k8s node节点报错 dial tcp 127.0.0.1:8080: connect: connection refused
前言 在搭建好 kubernetes 环境后,master 节点拥有 control-plane 权限,可以正常使用 kubectl. 但其他 node 节点无法使用 kubectl 命令,即使同步过 ...
- go mod 安装bee 报错
报错信息 go: github.com/beego/bee imports github.com/beego/bee/cmd imports github.com/beego/bee/cmd/comm ...
- 学习Linux只要学会这个命令就够了!
大家好,我是良许. 这段时间又是搬家,又是找新办公室,现在终于安顿下来了,有时间给大家分享干货了. 今天给大家介绍一个 Linux 超级实用命令,有了这个命令,你就可以愉快使用 Linux 上几乎所有 ...