一、Python内存管理(引用计数、垃圾回收)
Python(CPython)采用的是:
“引用计数为主,垃圾回收为辅” 的内存管理机制。
也就是说:
-
引用计数机制:负责大部分内存释放,简单、高效。
-
垃圾回收机制(GC):处理循环引用等引用计数解决不了的问题。
1. 引用计数(Reference Counting)
每个 Python 对象都有一个“引用计数”,表示当前有多少个地方在使用它。
一旦引用计数为 0,Python 立刻销毁对象、释放内存。
举例说明:
a = [1, 2, 3] # 创建列表对象,引用计数 = 1
b = a # b 也指向这个列表,对象引用计数 = 2
del a # 删除 a,引用计数 = 1
del b # 删除 b,引用计数 = 0,对象被销毁
可以使用 sys.getrefcount()
查看一个对象的引用计数:
import sys
x = [1, 2, 3]
print(sys.getrefcount(x)) # 注意:结果比你预期的多1,因为参数x也算一次引用
引用计数的优点:
-
实时释放内存:对象不用的时候,立刻回收。
-
实现简单:不需要像 Java 那样复杂的 GC 跑一遍遍。
2. 引用计数的缺陷:循环引用
如果两个对象互相引用对方,即使外部没有引用了,引用计数也不会为0,内存就不会释放,导致 内存泄露。
示例:
class Node:def __init__(self):self.ref = Nonea = Node()
b = Node()
a.ref = b
b.ref = adel a
del b# a和b虽然没有外部引用,但它们互相引用,所以内存不会释放!
在这个例子中:
对象 | 谁引用了它? | 引用计数 |
---|---|---|
a | main 作用域、b.ref | 2 |
b | main 作用域、a.ref | 2 |
现在执行 del a
、del b
:
对象 | 谁引用了它? | 引用计数 |
---|---|---|
a | 只剩 b.ref | 1 |
b | 只剩 a.ref | 1 |
引用计数永远不会变为 0!
3. 垃圾回收(Garbage Collection,GC)
为了解决循环引用的问题,Python 内部设计了一个 GC模块 来“定期清理”不再使用的内存。
这个 GC 是通过 “分代回收(Generational GC)” 实现的。
分代回收模型(Generational GC)
Python 把所有的对象分成 3 代:
代数 | 含义 | 特点 |
---|---|---|
第0代 | 新建对象 | 最频繁回收 |
第1代 | 经常存活下来的对象 | 偶尔回收 |
第2代 | 长期存在的对象 | 很少回收,数量最多 |
GC 机制假设:
“活得越久的对象,越有可能继续活着”,所以越老的代回收越少。
回收的流程
-
Python 会监控“分配了多少个对象”和“删除了多少个对象”。
-
当分配/删除数量超过阈值,就触发 GC:
-
先回收第0代
-
如果仍然触发阈值,就继续回收第1代、第2代
-
-
采用标记-清除算法:标记“可达对象”,删除“不可达对象”
GC 模块使用示例:
import gc# 查看当前是否启用了自动GC
print(gc.isenabled()) # True# 手动触发一次GC
gc.collect()# 查看GC统计信息
print(gc.get_count()) # 返回 (第0代对象数量, 第1代, 第2代)
4. 小对象内存池机制
CPython 还对**小对象(小于 512 字节)**做了优化:
-
使用内存池(称为 pymalloc)重复利用内存块。
-
这样避免频繁向操作系统申请/释放内存,提高性能。
这就是为什么写 a = 10; b = 10
时,两个变量可能指向的是同一个对象地址。
5. 总结
机制 | 原理 | 优点 | 缺点 |
---|---|---|---|
引用计数 | 每个对象维护引用计数,0即销毁 | 实时、高效 | 不能处理循环引用 |
垃圾回收 | 分代收集,标记清除 | 弥补引用计数的缺陷 | 增加一些系统开销 |
小对象池 | 内存池优化小对象 | 提高小对象复用效率 | 占用一些额外内存 |
二、Python解释器(CPython、PyPy)
Python 解释器就是把你写的 .py
源码翻译为计算机能执行的“指令”的程序。
目前主流解释器有多个实现:
名称 | 语言实现 | 特点 | 适用场景 |
---|---|---|---|
CPython | 用 C 写的 | 官方标准实现,最常用 | 默认解释器,稳定 |
PyPy | 用 RPython 写 | 支持 JIT,速度更快 | 追求性能 |
Jython | 用 Java 写 | 可运行在 JVM 上,调用 Java | Java 环境 |
IronPython | .NET 实现 | 支持 C#/VB 调用 | .NET 环境 |
MicroPython | C实现(精简) | 运行在嵌入式设备 | 单片机开发 |
1. CPython(最主流、最重要)
-
CPython 是 Python 的官方实现。
-
是用 C语言 写的解释器(Interpreter)。
-
所有你运行
.py
的地方,默认就是 CPython。
$ python3 --version
Python 3.11.7 ← 这就是 CPython
CPython 的执行流程
Python 源码(.py 文件)↓
词法/语法分析↓
AST(抽象语法树)↓
编译成字节码(.pyc 文件)↓
交由 CPython 的虚拟机解释执行
这也是为什么会看到 .pyc
文件,它其实就是:
Python 的“中间语言”,类似 Java 的 .class
文件。
CPython 底层结构
typedef struct _object {Py_ssize_t ob_refcnt; // 引用计数PyTypeObject *ob_type; // 类型信息
} PyObject;
CPython(Python 的官方解释器)就是用 C 语言编写的
Py_ssize_t ob_refcnt; // 当前对象的引用计数,决定是否自动释放
PyTypeObject *ob_type; // 指向该对象的类型结构体,比如 int、str 类型
每个 Python 中的对象(int、list、dict 等)在底层都是一个 PyObject 结构体,通过这个结构可以知道:
-
它被引用了多少次(用于自动内存管理)
-
它到底是什么类型(用于运行时类型识别)
CPython 的缺陷:GIL(全局解释器锁)
CPython 使用 GIL 来确保多线程安全:
Global Interpreter Lock→ 同一时间,只允许一个线程执行 Python 字节码
这意味着 Python 的多线程并不能真正并发运行计算密集型任务。CPU 利用率低。
2. PyPy(性能极致的解释器)
-
PyPy 是用一种叫 RPython(可静态类型的Python子集) 实现的 Python 解释器。
-
最大亮点是 JIT(Just-In-Time)编译技术,可以把热代码编译成机器码,加速运行。
PyPy 的优势
特性 | 说明 |
---|---|
JIT 编译 | PyPy 会将频繁执行的代码“编译为机器码”加速运行 |
智能优化 | 内联、移除多余变量等操作自动完成 |
⏱ 性能提升 | 实测一般比 CPython 快 4~10 倍 |
例如:
from time import timedef f():s = 0for i in range(10_000_000):s += ireturn sstart = time()
f()
print("耗时:", time() - start)
同样的代码,用 PyPy 跑会明显快很多。
如何下载安装 PyPy:
1)访问官网下载页面:https://www.pypy.org/download.html
2)解压缩到一个目录,比如:C:\pypy3.9
3)打开命令行,输入:
C:\pypy3.9\pypy3.exe
如果是 Linux / Mac 用户:
sudo apt install pypy3 # Ubuntu
# 或
brew install pypy3 # Mac(需安装 Homebrew)
4)运行代码用 PyPy 解释器
比如:
pypy3 your_script.py
或者:
pypy3
>>> print("hello from PyPy!")
这样就不再用默认的 CPython 。
三、编译原理初步(AST抽象语法树、字节码)
Python 作为一种 解释型语言,它的代码执行过程包括了“编译”和“解释”两个阶段:
编译阶段:把 Python 源代码编译成一种中间表示形式(字节码),这个字节码并不是直接机器码,而是 Python 虚拟机能理解的低级指令。
解释执行阶段:字节码由 Python 解释器(虚拟机)执行。
这两个过程中的重要组成部分就是 AST(抽象语法树) 和 字节码。
1. 什么是 AST(抽象语法树)?
AST(抽象语法树) 是对 Python 源代码的一种树状表示,它的每个节点代表了 Python 程序中的一个结构元素(如表达式、语句等)。它比语法树更简洁,去除了与编程语言具体语法无关的部分。
-
AST 是 Python 编译阶段的产物。
-
Python 代码首先通过 词法分析(将源码转换为标记)和 语法分析(将标记转换为 AST)两步生成。
-
在 AST 之上,Python 可以进行代码优化、分析、转换、生成字节码等操作。
AST 的重要性
-
代码分析与优化:通过 AST,你可以分析代码的结构,做静态分析(例如变量的使用、控制流等)或进行代码优化。
-
代码转换:AST 是生成字节码的基础,修改 AST 可以进行代码重写、反混淆等操作。
-
反向工程:通过分析 AST,你可以将混淆代码或压缩代码还原为易懂的原始代码结构。
生成 AST
可以使用 Python 的 ast
模块来分析和操作 Python 的 AST。例如,下面的代码将 Python 代码解析成 AST:
import astsource_code = "x = 1 + 2 * 3"
tree = ast.parse(source_code)# 打印 AST 的结构
ast.dump(tree, indent=4)
输出会是类似这样的结构(树状结构):
Module(body=[Assign(targets=[Name(id='x', ctx=Store())],value=BinOp(left=Num(n=1), op=Add(), right=BinOp(left=Num(n=2), op=Mult(), right=Num(n=3))))]
)
这就是 Python 代码的 AST 结构。可以看到,它把算式 1 + 2 * 3
转换成了一个树状结构,节点包含了操作符、操作数、表达式等。
AST 操作示例
通过操作 AST,你可以修改代码结构,比如实现代码重构、自动化修改等:
class MyTransformer(ast.NodeTransformer):def visit_BinOp(self, node):if isinstance(node.op, ast.Add):node.op = ast.Sub() # 把加法变成减法return node# 修改 AST
transformer = MyTransformer()
transformed_tree = transformer.visit(tree)# 查看修改后的树
ast.dump(transformed_tree, indent=4)
2. 什么是字节码(Bytecode)?
字节码 是一种中间代码,Python 将源代码编译成字节码后,由 Python 的虚拟机(PVM)解释执行。它比源代码更接近机器码,但仍然独立于平台。
-
字节码的作用:通过将源码编译成字节码,Python 可以实现跨平台运行,只需要安装 Python 解释器,就能执行相同的字节码。
-
.pyc
文件:字节码通常保存在.pyc
文件中,位于__pycache__
目录下。Python 会根据文件的修改时间来决定是否重新编译。
字节码的生成过程
-
编译:Python 源代码通过 CPython 编译器转换为字节码(
.pyc
文件)。每次你运行.py
文件时,CPython 会首先检查是否有.pyc
文件,如果没有就编译成字节码。 -
执行:字节码通过 Python 虚拟机(PVM)解释执行。
字节码反汇编
使用 dis
模块,可以看到 Python 代码的字节码。例如:
import disdef example():a = 1 + 2return adis.dis(example)
输出结果会显示字节码:
2 0 LOAD_CONST 1 (1)2 LOAD_CONST 2 (2)4 BINARY_ADD6 STORE_NAME 0 (a)8 LOAD_NAME 0 (a)10 RETURN_VALUE
这说明:
-
先把常量
1
和2
加载到栈上(LOAD_CONST
), -
执行加法(
BINARY_ADD
), -
把结果存到变量
a
中(STORE_NAME
)。
字节码与机器码的关系
字节码比源代码更接近机器码,但它仍然不是直接可以由 CPU 执行的代码。在 CPython 中,字节码是由 Python 解释器逐条解释执行的。而机器码则是直接由硬件执行的代码。
3. 结合 AST 和 字节码的实际应用
应用场景:代码重构、反混淆
在做 JS 逆向、APP 逆向时,很多时候需要分析混淆代码并重构它。在 Python 中,AST 是一个非常好的工具,可以帮助理解代码的结构,进行重构、逆向等操作。
例子:重构混淆代码
比如,将一段复杂的运算式改写为更易读的形式:
# 混淆的代码
x = 1 + 2 * 3# 解析并重构为
x = 7
通过 AST,可以提取出原始的运算结构,并将它们转换为更容易理解的形式。
应用场景:性能优化
在性能优化中,了解 Python 如何将源代码转化为字节码,有助于优化代码,避免不必要的计算、内存分配等操作。例如,避免频繁的内存分配、减少不必要的对象创建,可以提高程序的性能。
术语 | 含义 | 作用 |
---|---|---|
AST | 抽象语法树,一种树状结构 | 代码结构分析、重构、静态分析、代码生成和优化 |
字节码 | 中间代码,比源码更低级,但不依赖平台 | Python 跨平台运行,虚拟机解释执行 |
四、C扩展开发(如需要极限优化)
C 扩展就是使用 C 语言编写的 Python 模块,它可以被 Python 调用,像普通模块一样 import
。
为什么要用 C 写 Python 模块?
-
Python 本身是用 C 写的(CPython)
-
Python 解释型的性能较低,尤其在数值计算、循环密集任务中较慢
-
C 语言性能高,可以编写性能关键模块,再用 Python 调用
-
还能直接调用操作系统或硬件资源,或者加载第三方底层库(如 OpenSSL)
实际例子:写一个简单的 C 扩展模块
创建一个 C 模块 myfast.c
,提供一个加法函数给 Python 调用。
文件结构:
myfast/
├── myfast.c
└── setup.py
1) myfast.c
内容如下:
#include <Python.h>// C函数:两个数相加
static PyObject* add(PyObject* self, PyObject* args) {int a, b;// 从 Python 传入参数解析为 C 中的 intif (!PyArg_ParseTuple(args, "ii", &a, &b)) {return NULL;}// 返回一个 Python 整数return PyLong_FromLong(a + b);
}// 定义方法表
static PyMethodDef MyFastMethods[] = {{"add", add, METH_VARARGS, "Add two numbers"},{NULL, NULL, 0, NULL}
};// 定义模块
static struct PyModuleDef myfastmodule = {PyModuleDef_HEAD_INIT,"myfast", // 模块名NULL, // 文档(可为 NULL)-1,MyFastMethods
};// 初始化模块
PyMODINIT_FUNC PyInit_myfast(void) {return PyModule_Create(&myfastmodule);
}
2) setup.py
构建脚本:
from setuptools import setup, Extensionmodule = Extension('myfast', sources=['myfast.c'])setup(name='myfast',version='1.0',description='A demo C extension for Python',ext_modules=[module]
)
3)编译模块
运行:
python setup.py build_ext --inplace
这会生成一个 .so
文件(Linux/mac)或 .pyd
(Windows),然后就可以像普通模块一样导入:
4)Python 中使用
import myfastprint(myfast.add(3, 5)) # 输出 8
高级应用方向
调用已有 C/C++ 库(如 OpenSSL、libcurl)
可以封装 C 函数为 Python 接口,甚至是使用 extern
引用已有 .so
/.dll
。
写“黑盒”模块
很多商业代码、加密算法,甚至反爬参数逻辑会被写成 C 模块,然后 只暴露一个接口给 Python,大大提高逆向难度。
用于性能关键场景
在图像处理、数据加密、音视频处理、矩阵运算、机器学习中,Python 常调用 NumPy(内部也是 C 实现)。