Python 拼字符串为什么别在循环里用 +=?拆开 join 的两遍账:191.8 微秒与 6.8 微秒

写 Python 的人几乎都听过一句话:字符串不可变,循环里别用 += 拼接,用 join。

这句话是对的,但很多文章只说结论,不告诉你贵在哪。本文用 1000 段字符串的实测,把 += 和 join 的账拆到底,顺手验证一个常被说错的细节:CPython 对 += 的"原地优化"到底在什么条件下失效。

一、结论先行

  • 循环 += 拼接 1000 段:191.8 µs;list.append 后 join:45.1 µs;已有列表直接 join:6.8 µs
  • += 贵的根源是每一轮都可能复制整个已拼好的字符串 ;join 先扫一遍算总长,再一次性分配
  • CPython 对 s += x 有原地扩容优化,但它依赖引用计数条件 :手工加一个别名后,同样循环涨到 459.8 µs(扣掉别名基线约 442.6 µs,是纯 += 的 2.3 倍)
  • 少片段场景(3 段)别迷信 join :f-string 约 251.8 ns,% 约 249.1 ns,join 列表约 306.4 ns,str.format 约 490.8 ns

一句话:循环里攒字符串用 join;只有两三个片段要拼,用 f-string 就行。

二、先看实测:1000 段拼接

测试条件:1000 段、每段 8 个字符,timeit 取 7 轮中位数。

写法 µs/次(1000 段) 相对
s = '' 循环 s += p 191.8 基准
buf.append(p) 后 ''.join(buf) 45.1 快 4.3 倍
已有列表直接 ''.join(parts) 6.8 快 28 倍
''.join([str(i) for i in range(1000)]) 134.9 ---
每 100 段 join 一次(分块) 136.0 不如一次性 join

注意最后一行:分块 join 并没有更快。它多了一次中间列表构造和多次 join 调用,反而从 45.1 µs 涨到 136.0 µs。只有当数据本身是流式到达、无法一次性拿到全部片段时,分块 join 才值得考虑。

三、为什么 join 快:两遍账

str.join 的实现思路是两遍:

  1. 第一遍遍历所有片段,累加每个字符串的长度,得到结果串的总长
  2. 一次性分配这块内存,再把每个片段拷进去

而 s += p 每一轮做的是"造一个新字符串,把旧内容复制进去,再追加新片段"。随着 s 越来越长,每轮要复制的内容也越来越多------这是二次级别的复制成本,不是 1000 次小操作。

所以差距不是"join 的 C 循环更快"这么简单,而是分配次数和复制量级的差距。

四、字节码:+= 并没有特殊指令

scss 复制代码
LOAD_FAST_LOAD_FAST   16 (s, x)
BINARY_OP             13 (+=)
STORE_FAST            1 (s)

3.13 里 s += x 编译成 BINARY_OP 13 (+=),没有专门的"原地拼接"指令。真正的优化发生在解释器执行这条指令时:当左侧是 str 且引用计数允许复用时,CPython 会尝试原地扩容而不是新建一个对象。

五、原地优化什么时候失效

验证方法:在每轮拼接前加一个别名 t = s,让对象多一个引用,再删掉别名。

写法 µs/次
纯 s += p 191.8
t = s 后 s += p,再 del t 459.8
只做 t = s 和 del t(基线,不拼接) 17.2

扣掉 17.2 µs 的别名操作基线,破坏引用计数条件后约 442.6 µs,是纯 += 的 2.3 倍。

这也解释了为什么这个优化"看不见摸不着":本次还测了 200 次 += 后的 id(s),出现了 165 个不同 id。也就是说,扩容时对象地址可能变化,不能靠 id 是否相同判断有没有原地优化------真正的证据是上面这组时间差。

六、少片段场景:join 不一定赢

把片段减到 3 个,结论会反转:

写法 ns/次
% 格式化 249.1
f-string 251.8
''.join([...]) 306.4
a + ' ' + str(b) + ' ' + c 312.1
str.format 490.8

原因很直白:join 要先构造一个列表 ,这层开销在只有 3 个片段时盖过了它的复制优势。str.format 最慢,是因为它要走完整的格式规范解析。

所以不要背"永远用 join"。片段多、来自可迭代对象 → join;片段少、写死在代码里 → f-string。

七、实战守则

  • 循环里绝不 += 攒字符串 :用一个列表收集,最后一次性 join
  • 已有列表就直接 ''.join(list) ,不要在 join 里再写一遍列表推导(除非本来就要转换类型)
  • 别为了"优化"做分块 join:实测更慢,只在流式场景下用
  • 日志/告警里的拼接先看量级:3 个片段用 f-string,几十上百个片段用 join
  • 别依赖 += 的原地优化 :它依赖引用计数条件,一旦有人持有旧引用就失效;也不要用 id 去验证它

一句话总结:字符串拼接的成本不在"加",在"复制"。join 用两遍扫描换一次分配,这就是它比循环 += 快 28 倍的全部秘密。

相关推荐
~~刘杨1 小时前
用 Python 批量处理商品图:抠图、高清放大与图生视频(同步 API 完整示例)
图像处理·python·aigc
CodeGL1 小时前
15、Python 访问数据库
数据库·python
用户298698530141 小时前
Python 实现 Markdown 与 Word 文档互转
python·api·markdown
EnCi Zheng1 小时前
AI Agent工具调用防护层怎么写-一份能直接跑的Python四层拦截实现
大数据·开发语言·python
2601_949847751 小时前
Cookie 跨端三方库仓颉适配全流程
python·django·flask
万粉变现经纪人1 小时前
如何解决 pip install 安装报错 检测到 yanked 版本需更换可用版本 问题
人工智能·python·scrapy·numpy·pandas·pyqt·pip
香蕉大香蕉(06敬畏市场)1 小时前
高频八股 · Java 并发编程(10):异步、ThreadLocal 与虚拟线程(收官)
java·开发语言·网络·jvm·python·ai·实习
JustNow_Man1 小时前
【Harness】本地文本向量化搜索解决方案--qmd
git·python·算法
小林ixn1 小时前
用 FastAPI + SQLAlchemy 从零手搓一个博客系统:一套能落地、能讲清楚的 RESTful 后端
后端·python·fastapi