在日常开发中,我们常常凭借直觉选择数据结构、编写函数、或决定是否使用异步。然而在Python中,那些”看起来差不多“的操作,实际开销可能相差几个数量级。
最近读到一篇文章《Python Numbers Every Programmer Should Know》,作者通过实测给出了常见操作的时间与内存开销,这些数字不仅令人惊叹,更能指导我们写出更高效、更省资源的代码。
本文将结合原文内容与实践经验,分享一些关键的性能认知与开发技巧。
一、内存开销:看不见的资源消耗
Python的便利性背后,是显著的内存开销。理解这些开销,是处理大数据集或高并发应用的前提。
1. 基础对象的“体重”
- 一个空的字符串
""重达 41字节。 - 一个小整数(如
1)占用 28字节(得益于-5到256的整数池缓存,相同小整数是共享的)。 - 一个空列表
[]需要 56字节,一个空字典{}需要 64字节。
2. 集合的容量增长
当存储1000个元素时,不同数据结构的内存效率差异显著:
- 列表(1000个整数):约36 KB
- 字典(1000个键值对):约91 KB (是列表的2.5倍)
- 集合(1000个成员):约60 KB
3. 类的内存激增
面向对象编程的便利伴随着成本。一个具有5个属性的普通类实例占用约 694字节。如果程序中需要创建成千上万个实例,内存压力会急剧上升。
二、常见操作的性能概览:从纳秒到毫秒
让我们看一下”性能金字塔”,从最快的操作到较慢的操作:
1 | 属性读取 (obj.x) 14 ns |
数据说话:
- 基本操作(属性访问、字典查询)在纳秒级
- 文件操作和数据库操作在微秒到毫秒级
- 模块导入可能比你想象的慢的多
三、数据结构选择:性能差距可达200倍
列表 vs 字典 vs 集合的成员检查,在 1000 个元素中查找一个元素:
| 数据结构 | 耗时 | 每秒操作数 |
|---|---|---|
集合item in set |
19 ns | 52.7M ops/sec |
字典item in dict |
22 ns | 45.7M ops/sec |
列表item in list |
3,850 ns | 259.6k ops/sec |
数据说话:
结论:列表比集合/字典慢 200 倍!本质原因:集合和字典使用哈希表,时间复杂度 O(1),列表需要线性扫描,时间复杂度 O(n)
实践建议
1 | # 不推荐:频繁的成员检查 |
四、列表推导式真的更快
- 对比生成 1000 个元素:
| 方法 | 耗时 | 性能提升 |
|---|---|---|
| 列表推导式 | 9.45 μs | +26% |
| for 循环 + append | 11.9 μs | 基准 |
数据说话:
- 原因:列表推导式在 C 层面优化,减少了 Python 层面的函数调用开销。
五、内存优化:强大的__slots__
单个对象的内存占用
| 类型 | 5个属性的实例 | 内存节省 |
|---|---|---|
| 普通类 | 694 bytes | - |
__slots__类 |
212 bytes | 69% |
| dataclass | 694 bytes | - |
| dataclass(slots=True) | 212 bytes | 69% |
1000 个实例的集合
| 类型 | 总内存 | 内存节省 |
|---|---|---|
| 1000 个普通实例 | 301.8 KB | - |
| 1000 个 slots 实例 | 215.7 KB | 28% |
使用 __slots__ 的示例:
1 | # 普通类 |
访问速度几乎相同:
- 普通类读取:14.1 ns
- slots 类读取:14.1 ns
何时使用 __slots__:
- 需要创建大量实例(数千个以上)
- 属性固定且已知
- 内存受限的环境(如游戏实体、缓存项、日志记录)
不适用场景:
- 需要动态添加属性
- 需要使用
__dict__或__weakref__
tips: Python便利性背后,是显著的内存开销。对于需要海量创建的小型数据对象,优先考虑使用
namedtuple、dataclass或手动定义__slots__,可以带来巨大的内存节约
六、JSON 序列化:选对库提速 8 倍
复杂对象序列化性能对比
| JSON 库 | 序列化耗时 | 反序列化耗时 | 相对标准库 |
|---|---|---|---|
| orjson | 310 ns | 839 ns | 8.5x 更快 |
| msgspec | 445 ns | 850 ns | 6x 更快 |
| ujson | 1.64 μs | 1.46 μs | 1.6x 更快 |
| json (标准库) | 2.65 μs | 2.22 μs | 基准 |
数据说话:
- Python 的标准库稳定可靠,但在性能敏感场景,第三方库往往更胜一筹。如果你的 API 需要高频返回 JSON,换一个序列化库可能就解决了瓶颈。
七、Web 框架性能对比
返回简单 JSON 响应的基准测试:
| 框架 | 平均响应时间 | 每秒请求数 | 相对性能 |
|---|---|---|---|
| Starlette | 8.01 μs | 124.8k | 最快 |
| Litestar | 8.19 μs | 122.1k | 1.02x |
| FastAPI | 8.63 μs | 115.9k | 1.08x |
| Flask | 16.5 μs | 60.7k | 2.06x |
| Django | 18.1 μs | 55.4k | 2.26x |
数据说话:
- 现代异步框架(Starlette、FastAPI、Litestar)性能相近
- FastAPI 比 Django 快约 2 倍
- 传统同步框架(Flask、Django)相对较慢
框架选择建议:
- 高性能 API:Starlette、Litestar、FastAPI
- 快速原型:Flask
- 全功能应用:Django
- 性能不是唯一考量:还要考虑生态系统、团队熟悉度、项目需求
八、数据库操作性能
SQLite、diskcache、MongoDB 对比
| 操作 | SQLite | diskcache | MongoDB |
|---|---|---|---|
| 写入 | 192 μs | 23.9 μs | 119 μs |
| 主键读取 | 3.57 μs | 4.25 μs | 121 μs |
| 字段查询 | 4.27 μs | N/A | 124 μs |
| 更新 | 5.22 μs | 23.9 μs | 115 μs |
数据说话:
- SQLite 读取极快(微秒级),适合读多写少
- diskcache 写入最快,适合缓存场景
- MongoDB 受网络延迟影响,进程内数据库(SQLite)更快
九、字符串格式化:f-string 的优势
| 格式化方法 | 耗时 | 相对性能 |
|---|---|---|
字符串拼接 + |
39.1 ns | 最快(简单场景) |
| f-string | 64.9 ns | 推荐 |
%格式化 |
89.8 ns | 1.4x 慢 |
.format() |
103 ns | 1.6x 慢 |
数据说话:
- f-string 在性能和可读性之间达到了最佳平衡,应作为首选。
十、异步(Async)不是银弹,它有成本
同步 vs 异步函数调用
| 操作 | 耗时 | 倍数差异 |
|---|---|---|
| 同步函数调用 | 20.3 ns | 基准 |
| 异步函数调用 | 28.2 μs | 1,400x 慢 |
异步操作的开销
| 操作 | 耗时 |
|---|---|
| 创建协程对象 | 47 ns |
run_until_complete空协程 |
27.6 μs |
asyncio.sleep(0) |
39.4 μs |
gather10 个协程 |
55 μs |
关键认知:很多人以为“用了 async 就更快”,但事实并非如此。
- 启动一次空的
asyncio事件循环:~28 微秒 - 调用一个普通函数:~20 纳秒
- async 的启动开销比一次磁盘 I/O(~10 微秒)还要大!
这意味着:
- 如果你的任务是 CPU 密集型(如图像处理、数值计算),async 不仅没用,反而拖慢速度。
- 如果你只是调用一次数据库或 API,同步代码更简单、更快。
- 只有当你需要同时处理成百上千个 I/O 请求时(如聊天服务器、爬虫),async 才真正发挥价值。
十一、异常处理的成本
| 操作 | 耗时 | 倍数差异 |
|---|---|---|
| try/except(无异常) | 21.5 ns | 基准 |
| try/except(有异常) | 139 ns | 6.5x 慢 |
最佳实践:
1 | # 不要用异常控制正常流程 |
十二、必须警惕的“性能陷阱”
- 异常滥用:
try/except在无异常发生时开销很小(21.5 ns),但一旦触发并捕获异常,成本激增至约140 ns,是正常流程的6-7倍。切勿将异常用于常规控制流。 - 昂贵的导入:如前述,大型模块的首次导入成本极高。在设计需要快速启动的CLI工具或Lambda函数时,应考虑延迟导入或减少依赖。
- 列表的成员检查:在1000个元素的列表中检查成员,需要 3.85 µs,而等量集合的成员检查仅需 19 ns,快200倍以上。频繁的成员检查,务必使用集合(set)。
- 写文件 vs 读文件:写入1KB数据(35 µs)比读取(10 µs)慢3.5倍,比单纯打开关闭文件(9 µs)慢近4倍。对写操作密集的程序,缓冲和批量写入策略尤为重要。
写在后面
理解这些性能数字不是为了过早优化,而是为了在架构设计和关键路径上做出明智的决策。
“过早优化是万恶之源。” —— Donald Knuth
但同时,了解基本的性能特性可以帮助我们避免明显的性能陷阱.
性能优化的黄金原则:
- 先让代码正确运行
- 用
cProfile、py-spy等工具定位瓶颈 - 针对瓶颈做有针对性的优化
- 用基准测试验证效果
真正的性能艺术,是在开发效率、可读性与执行效率之间找到平衡。不必对每个纳秒斤斤计较,但要对数量级(倍数的差异)保持敏感**。
希望这些”应该知道的数字”,能帮助我们写出不仅正确,而且优雅高效的 Python 代码。
参考资源: