在日常开发中,我们常常凭借直觉选择数据结构、编写函数、或决定是否使用异步。然而在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
属性读取 (obj.x)                         14 ns
字典查询 22 ns
函数调用(空函数) 22 ns
列表追加 29 ns
f-string 格式化 65 ns
异常捕获 140 ns
orjson 序列化复杂对象 310 ns
json 反序列化简单对象 714 ns
遍历 1000 个元素的列表 7.9 μs
打开并关闭文件 9.1 μs
SQLite 主键查询 3.6 μs
写入 1KB 文件 35 μs
MongoDB find_one 121 μs
SQLite 插入(带提交) 192 μs
导入 json 模块 2.9 ms
导入 asyncio 模块 18 ms
导入 fastapi 模块 104 ms

数据说话:

  • 基本操作(属性访问、字典查询)在纳秒级
  • 文件操作和数据库操作在微秒到毫秒级
  • 模块导入可能比你想象的慢的多

三、数据结构选择:性能差距可达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
2
3
4
5
6
7
8
9
# 不推荐:频繁的成员检查
allowed_users = ['alice', 'bob', 'charlie', ...] # 列表
if user in allowed_users: # 慢!
process()

# 推荐:使用集合
allowed_users = {'alice', 'bob', 'charlie', ...} # 集合
if user in allowed_users: # 快 200 倍!
process()

四、列表推导式真的更快

  • 对比生成 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 普通类
class User:
def __init__(self, name, email, age, city, score):
self.name = name
self.email = email
self.age = age
self.city = city
self.score = score

# 使用 __slots__(节省 69% 内存)
class User:
__slots__ = ['name', 'email', 'age', 'city', 'score']

def __init__(self, name, email, age, city, score):
self.name = name
self.email = email
self.age = age
self.city = city
self.score = score

访问速度几乎相同

  • 普通类读取:14.1 ns
  • slots 类读取:14.1 ns

何时使用 __slots__

  • 需要创建大量实例(数千个以上)
  • 属性固定且已知
  • 内存受限的环境(如游戏实体、缓存项、日志记录)

不适用场景:

  • 需要动态添加属性
  • 需要使用 __dict____weakref__

tips: Python便利性背后,是显著的内存开销。对于需要海量创建的小型数据对象,优先考虑使用namedtupledataclass或手动定义__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

数据说话:

  1. 现代异步框架(Starlette、FastAPI、Litestar)性能相近
  2. FastAPI 比 Django 快约 2 倍
  3. 传统同步框架(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
gather

10 个协程
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 不要用异常控制正常流程
def get_value(d, key):
try:
return d[key]
except KeyError:
return None # 异常路径慢 6.5 倍

# 使用条件判断
def get_value(d, key):
if key in d:
return d[key]
return None

# 或使用 dict.get()
def get_value(d, key):
return d.get(key)

十二、必须警惕的“性能陷阱”

  1. 异常滥用try/except 在无异常发生时开销很小(21.5 ns),但一旦触发并捕获异常,成本激增至约140 ns,是正常流程的6-7倍。切勿将异常用于常规控制流。
  2. 昂贵的导入:如前述,大型模块的首次导入成本极高。在设计需要快速启动的CLI工具或Lambda函数时,应考虑延迟导入或减少依赖。
  3. 列表的成员检查:在1000个元素的列表中检查成员,需要 3.85 µs,而等量集合的成员检查仅需 19 ns快200倍以上频繁的成员检查,务必使用集合(set)
  4. 写文件 vs 读文件:写入1KB数据(35 µs)比读取(10 µs)慢3.5倍,比单纯打开关闭文件(9 µs)慢近4倍。对写操作密集的程序,缓冲和批量写入策略尤为重要。

写在后面

理解这些性能数字不是为了过早优化,而是为了在架构设计和关键路径上做出明智的决策。

“过早优化是万恶之源。” —— Donald Knuth

但同时,了解基本的性能特性可以帮助我们避免明显的性能陷阱.

性能优化的黄金原则:

  1. 先让代码正确运行
  2. 用 cProfilepy-spy 等工具定位瓶颈
  3. 针对瓶颈做有针对性的优化
  4. 用基准测试验证效果

真正的性能艺术,是在开发效率、可读性与执行效率之间找到平衡。不必对每个纳秒斤斤计较,但要对数量级(倍数的差异)保持敏感**。

希望这些”应该知道的数字”,能帮助我们写出不仅正确,而且优雅高效的 Python 代码。

参考资源


本站由 BluesSen 使用 Stellar 1.33.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本站总访问量