selfdb:把整个Python程序变成可查询数据库的魔法库
> selfdb 是一个将 Python 程序运行时状态自动暴露为 SQL 可查询数据库的开发工具,让你用 SQL 直接查询内存中的对象、函数调用和变量。
## 痛点:调试和观测 Python 程序为何如此痛苦
在开发 Python 应用时,我们经常面临这样的困境:
- **调试复杂状态**:当程序运行到某个分支时,你想知道当前所有模块的全局变量、某个对象的属性、甚至最近几次函数调用的参数——但只能靠 `print` 或 `pdb` 手动断点,效率低下。
- **事后分析**:程序崩溃后,你想复盘“当时发生了什么”,但日志不够详细,回放困难。
- **性能分析**:你想知道某个函数被调用了多少次、平均耗时多少,通常需要引入 `cProfile` 或手动埋点。
- **动态数据探索**:你希望像操作数据库一样,用 `SELECT` 和 `JOIN` 去探索程序内部的数据关系,但 Python 对象图是任意复杂的,没有统一查询接口。
传统方案要么是侵入式埋点(污染业务代码),要么是外部调试器(无法访问运行时上下文),要么是日志分析(丢失对象结构)。`selfdb` 提出一个全新的思路:**把整个 Python 进程变成一台数据库服务器**,你可以在程序运行的同时,用 SQL 查询它的内部状态。
## selfdb 是什么
`selfdb` 是一个纯 Python 库,它通过 `sys.settrace` 钩子自动捕获程序执行过程中的所有帧(frame)、局部变量、全局变量、函数调用和返回值,并将它们组织成一张张关系表。然后,它启动一个内置的 SQLite 虚拟数据库(通过 `sqlite3` 的虚拟表机制),让外部客户端(如 `sqlite3` 命令行、Python `sqlite3` 模块、甚至其他语言的 SQLite 驱动)直接连接并查询这些表。
简单说:**你的程序运行到一半,你可以用 SQL 去查询它现在的内存状态。**
## 快速上手
### 安装
bash
pip install selfdb
### 最小示例:查询全局变量
python
import selfdb
import threading
import time
# 启动 selfdb,监听在本地 12345 端口(默认)
selfdb.start(port=12345)
# 程序正常运行
counter = 0
def worker():
global counter
for i in range(100):
counter += 1
time.sleep(0.01)
threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
t.start()
# 保持程序运行,等待外部查询
selfdb.wait() # 阻塞,直到 Ctrl+C
在另一个终端,用任意 SQLite 客户端连接:
bash
sqlite3 selfdb.db
或者通过 Python 连接:
python
import sqlite3
conn = sqlite3.connect('selfdb.db')
# 查看有哪些表
print(conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall())
# 查询全局变量 counter 的当前值
rows = conn.execute("SELECT * FROM globals WHERE name='counter'").fetchall()
print(rows)
你会发现 `globals` 表中有当前所有全局变量的名称、类型和值。更神奇的是,你可以实时查询——每次 `SELECT` 都会获取最新状态。
### 查询函数调用历史
selfdb 会自动记录每次函数调用的参数和返回值,存到 `calls` 表:
sql
SELECT function_name, args, return_value, duration_ms
FROM calls
ORDER BY timestamp DESC
LIMIT 10;
你可以看到最近 10 次函数调用的完整记录,包括参数值、返回值和耗时。这对于调试性能瓶颈和异常流程非常有用。
### 查询局部变量和帧信息
selfdb 还记录了每个活动线程的调用栈(stack frames)。你可以查询 `frames` 表:
sql
SELECT thread_id, filename, lineno, function, locals_json
FROM frames
WHERE thread_id = 5;
`locals_json` 是该帧的所有局部变量序列化后的 JSON 字符串。这样,你可以看到某个线程当前执行到哪一行,局部变量是什么。
## 核心亮点:深入技术细节
### 1. 基于 `sys.settrace` 的零侵入捕获
selfdb 利用 Python 的追踪钩子(`sys.settrace`)在每次函数调用、行执行、返回时触发回调。它不修改你的源代码,也不要求你显式地记录任何数据。你只需要 `import selfdb; selfdb.start()`,剩下的自动完成。
需要注意的是,`sys.settrace` 本身会带来一定的性能开销(通常 20%-50%),所以 selfdb 更适合开发调试和测试环境,不适合生产环境长期开启。但它的设计允许你通过环境变量或命令行参数动态开启/关闭,例如:
bash
SELFDB_ENABLED=1 python my_app.py
### 2. 虚拟表:SQLite 与 Python 对象的桥梁
selfdb 没有把数据复制到 SQLite 文件,而是通过 SQLite 的虚拟表机制(`sqlite3` 模块中的 `create_module`)动态生成查询结果。每次你执行 `SELECT`,它都会实时从 Python 的内存中提取数据并返回。这意味着:
- **数据永远是最新的**:你看到的就是程序此刻的状态。
- **无持久化开销**:不需要写文件,不占用磁盘。
- **支持复杂 SQL**:你可以对查询结果进行 `WHERE`、`JOIN`、`GROUP BY` 等操作,因为虚拟表的行为和普通表一样。
### 3. 支持多线程和异步
selfdb 能正确区分不同线程的帧和局部变量。对于 `asyncio` 协程,它也能捕获事件循环中的任务状态(通过钩住 `asyncio.Task` 的创建和切换)。
### 4. 安全性和访问控制
默认只监听 127.0.0.1,避免远程访问风险。你可以通过 `selfdb.start(host='0.0.0.0')` 开启远程,但需要自己加认证(建议通过 SSH 隧道)。
### 5. 可扩展的表结构
除了内置的 `globals`、`locals`、`frames`、`calls` 表,selfdb 还提供 `objects` 表,让你按类名查询所有实例:
sql
SELECT class_name, id, repr, attributes_json
FROM objects
WHERE class_name = 'MyClass';
这会遍历 `gc.get_objects()` 找到所有 `MyClass` 的实例,并序列化它们的属性。
## 适用场景
1. **复杂算法调试**:比如图算法、动态规划,你想在每一步检查中间状态,用 SQL 比用 `print` 更灵活。
2. **Web 服务开发**:在 Flask/Django 开发中,你可以开启 selfdb,然后通过 SQL 查询当前请求的上下文、数据库连接池状态、缓存命中率等。
3. **数据分析脚本**:你在 Jupyter 中运行长任务,想中途查看某个变量的分布,直接用 SQL 查询。
4. **教学演示**:向学生展示 Python 程序内部执行过程,用 SQL 查询帧和变量,直观且有趣。
5. **自动化测试**:在测试框架中,你可以用 selfdb 断言某个函数被调用了多少次、参数是否正确,而不用 mock。
## 与其他工具对比
| 工具 | 定位 | 与 selfdb 的区别 |
|------|------|------------------|
| `pdb`/`ipdb` | 交互式调试器 | 需要手动打断点,只能查看当前帧,不能跨线程/跨时间查询。selfdb 提供全局、实时的 SQL 接口。 |
| `py-spy` | 采样性能分析器 | 只给出 CPU 火焰图,无法查询变量值。selfdb 能给出具体数据。 |
| `tracemalloc` | 内存跟踪 | 只关注内存分配,不提供业务对象状态。 |
| `logging` | 日志记录 | 需要手动埋点,且日志是文本流,无法结构化查询。selfdb 自动捕获所有调用和变量。 |
| `django-debug-toolbar` | Web 调试面板 | 只适用于 Django,且只显示 SQL 查询和请求信息,不能查询任意 Python 对象。 |
| `ipython` 的 `%debug` | 事后调试 | 只能在异常后进入,不能实时查询。 |
selfdb 的核心差异化在于:**它把调试从“断点思维”转变为“数据库思维”**。你不再需要关心“停在哪一行”,而是可以随时问“现在全局有哪些对象?它们之间的关系是什么?”
## 局限性与注意事项
- **性能开销**:开启追踪后,程序运行速度会明显下降,不适合生产环境。
- **序列化限制**:某些对象(如生成器、socket、线程锁)无法被序列化为 JSON,selfdb 会用其 `repr` 字符串代替。
- **安全性**:暴露了程序内部所有数据,切勿在不可信网络下开启远程连接。
- **Python 版本**:目前支持 CPython 3.7+,不兼容 PyPy 或 Jython(因为 `sys.settrace` 行为差异)。
## 总结
selfdb 是一个极具创意的开发工具,它模糊了“程序运行”和“数据库查询”的边界。虽然它可能不会成为生产环境中的常驻工具,但在开发、调试、测试和教学场景中,它提供了一种全新的、高效的交互方式。如果你厌倦了 `print` 和断点调试,不妨试试用 SQL 来“审视”你的程序。
项目链接:https://github.com/fzakaria/selfdb