起因:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import inspect import sys import pdb from A import A from B import test_a class Test( object ): print 'TEST EXE' def echo( self ): print 'hello' pass def makeTest(): # from test import Test t = Test() print 'makeTest' print inspect.getmodule(t), t.__module__, id (Test) checkTest(t) return t def checkTest(t): from Test import Test def echo_hot( self ): print 'hot' Test.echo = echo_hot print 'checkTest' pdb.set_trace() print isinstance (t, Test), inspect.getmodule(t), Test.__module__, id (Test) t.echo() if __name__ = = '__main__' : print 'A id from main: ' , id (A) makeTest() |
cpython源码调试
为了跟踪特定 python 写法对应到 cpython 的逻辑,我们需要在运行 python 代码的时候,能够跟踪到cpython源码层的各个细节,变量等等,也就能找到python一些奇怪语法对应的实现原理。
通过 python dis 模块,我们可以很容易得到一份python代码对应的字节码:
比如,我们上边的那段代码的 isinstance 函数就是Python源码内处理内置函数 bltinmodule.c 文件内的一个函数。
找一份 cpython 源码,编译好,带调试符号的,然后我们可以轻松的在 builtin_isinstance 处打断点,似乎很友善,然而跑起来就发现会有无数的逻辑会跑到这个断点,基本都是cpython虚拟机内部的一些库啊,函数啊啥的,很难定位到我们上面那段代码执行的时间点。
因此,我们的问题就是如何在python代码运行的恰当时间点,在cpython源码触发断点。这个问题下有人给了解决方案
https://stackoverflow.com/questions/41160447/cant-enable-py-bt-for-gdb
方法是自己写一个 cpython 插件,然后在python代码中需要断点的地方调用自己写的函数,在插件的c代码处打断点,自己写的插件当然不会有其它地方触发,我们也在恰当的时间点中断cpython执行。当然我们有现成的工具pdb,pdb的 set_trace 可以实现上面回答中的cpython插件功能。联调的步骤也就变成了下面这样。
1. 在要调试的python代码位置,设置 pdb.set_trace()
2. 启动 gdb 加载带调试符号的 python 虚拟机
3. 在 gdb 内执行对应的 python 代码
4. 在 python 代码运行到 pdb trace 的时候会触发pdb中断
5. 这个时候另起个终端,给cpython发送个信号, pkill python -SIGTRAP
6. gdb 就会退回到调试窗口
7. 在 gdb 内设置断点
8. continue 回到 pdb
9. pdb 内用 n + enter 继续执行
就断到了 cpython 代码里,在这儿就可以用 gdb 看当前 python frame 的各种状态了。
所以上面的Test代码到底发生了啥
在上面的代码内我们定义了个 python class,叫做 Test,如果我们直接在python控制台 import 文件后通过 dis.dis 查看字节码看到的应该是这样的,并没有预想到的看到 BUILD_CLASS 字节码。
所以当我们 import python模块的时候, BUILD_CLASS 这个过程是如何触发的呢,这需要通过pyc文件dis才能看得到,具体可以参考:
然后就可以找到 BUILD_CLASS 和 STORE_NAME 相关的字节码
BUILD_CLASS 是创建一个类的过程,STORE_NAME 是把创建好的类对象存放到 f -> f_locals 作用域里
而当我们通过 run_pyc_file 运行一个 pyc 文件的时候,传入的和 globals 和 locals 相同
通过 gdb 看到的地址也印证了这一点
也就是说当我们通过 run_pyc_file 运行一个py module 的时候,定义的 class 会同时存放在 global 和 locals 里。关于 globals 和 locals,只有在调用函数的时候,globals 传的是 globals 地址,locals传的是NULL,这时候 globals 和后续的 locals 才发生分化。
当我们在 makeTest 构造一个Test的对象时,使用的字节码是,LOAD_GLOBAL 找到对应的类, 然后执行类的构造
而当我们在 checkTest 内通过 from ... import 的方式引入一个 Class 时首先会调 IMPORT_NAME 把 module/package 导入,这里是 Test 模块,然后会在 Test模块内查找Test类
IMPORT_NAME 的逻辑一开始还是不太好看懂的,只是知道最后会在 import.c 代码里完成 import ,于是在下面这个函数内加了个断点。
中断后,trace 就是下面这样
可以看出,IMPORT_NAME 是调用 PyEval_CallObject 然后调用 builtinimport来完成 package 导入的。
而在 PyImport_ExecCodeModuleEx 内,会根据 name 从 sys.modules 里找是否已经 load 了该模块,值得注意的是,这里边函数参数 name 是没有 sys.path 前缀的路径名字,这个例子里就是 ‘Test’,这个名字也就是 sys.modules 里的键值。
import 最后执行的就是在 module 空间内执行模块代码,module空间传入的globals和locals都是 sys.modules[..].md_dict 因此,modules里的module的 dict 里也就有了相关类的实例。
所以,例子中奇怪的表现就可以解释了,我是把 Test 作为 main module 的,因此执行时,Test类在 main module里实例化了,后面再 from .. import 时,module就不是 main了,因此Test 类在 sys.modules 里有了多份实例,当然多个 Class 实例的 id 是不同的,这个似乎可以理解,毕竟指针也不一样。我本以为isinstance会做些处理,虽然引用的路径不同,但终归是一份代码而且没有reload过,返回test对象是通过 from .. import 方式导入的Class的对象似乎合情合理,但结果还是有点诧异的。所以最后又看了下,Python内置函数isInstance是如何检测一个对象是否属于一个类。
最终 PyObject_IsInstance 会调用到 PyClass_IsSubclass,然后这里边是直接用 kclass 和 base 进行指针比较。。。也就是说同一个声明的多个Python类对象所定义的对象也属于不同的类,只要import的时候旧import的class已经不存在了,或者找不到了,或者找的方式不对。
其实我们可以简化出另一个测试例子:当sys.path 内有多条路径找到一个 class 时,而代码又通过不同的路径去 import 这些class时,sys.modules 就会缓存多个 class 对象,然后多个 class 实例化的对象也就所属不同的类。
目录结构
从不同路径导入 package
sys.modules里就有了不同的 package key,而A类就会在不同的类下有了实例。
虽然是同一份代码,对于习惯 C++,java之类的,everything is object 属实很坑。。。
总结
单调试 python 挺好调试的,单调试 cpython 也挺方便的,但是在python运行的某个时间点想看看cpython的各种状态要稍微麻烦点,需要cpython在特定的时间点中断给设置gdb breakpoint提供机会,pdb可以做,自己写的插件也可以处理。而关于python本身,没有系统学习过,所以会经常遇到自己感觉很奇怪的语法,也许只是自己不够pythonic吧,遇到这种问题不跟到源码始终感觉莫名其妙。
参考:
http://www.xumenger.com/01-python-pyc-20180521/
https://stackoverflow.com/questions/41160447/cant-enable-py-bt-for-gdb
https://medium.com/@skabbass1/how-to-step-through-the-cpython-interpreter-2337da8a47ba
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具