写了三年 Python,自认为字符串拼接玩得挺溜。直到上个项目重构,我才发现自己一直在用最低效的方式。
那天排查线上接口性能问题,一个简单的用户信息组装居然占了 200 毫秒。我盯着那段代码看了半天,不就是用加号把几个字符串拼起来。换成 join 方法后,响应时间直接降到 30 毫秒。当时后背就凉了。
大家最常用的 加号拼接 其实有个隐藏问题。每次执行 + 操作,Python 都会创建一个新的字符串对象,然后拷贝旧内容。循环里拼 1000 次,就生成 1000 个临时字符串。这些临时对象多了,内存碎片和 GC 压力都上来了。
有人说 1000 次不算什么,那试试 10000 次。我写了个测试脚本,拼接 10 万次字符串。加号用了 2.3 秒,join 方法' '.join(list) 只用了 0.03 秒。差了将近 80 倍。群里好几个人发了个冷汗的表情。
另一个坑是 f-string。它的性能比加号好不少,跟 .format() 差不多。但有人喜欢在循环里用 f-string 拼 SQL,比如 query = f'SELECT FROM table WHERE id = {i}'。这种做法不光性能差,还有 SQL 注入风险。正确的做法是用参数化查询,把变量传给数据库驱动去处理。
还有人迷恋 % 格式化,觉得老代码稳定。实际上 % 格式化现在既慢又容易出错。类型不匹配直接报错,拼接列表的时候还得手动转成元组。我见过有人因为忘记了这一点,生产环境半夜崩了。
最让我意外的是 字符串乘法 的用途。比如生成固定长度的分隔线,'-' 80 比循环拼接快一个数量级。同事看了代码说这写法看着奇怪,但性能确实好。
还有大家都忽视的 StringIO。在循环里反复拼接大量文本,用 from io import StringIO 创建缓冲区,最后调用 getvalue() 一次性拿结果。这个方法比加号快十倍,比 join 快一点。适合处理日志、报告这类大文本。
我后来把项目里所有循环拼接都改成了 join 或 StringIO。线上接口的 95 分位延迟从 400 毫秒降到了 150 毫秒。老板问我做了什么优化,我说就是把字符串拼接的方式改了一下。他不太信。
现在写代码会把字符串操作当成性能敏感点。静态文本用常量,动态拼接用 join,大量文本用 StringIO。格式化输出用 f-string,但只用在非循环场景。这样搞了半年,系统很少因为字符串相关的性能问题出异常。
有个新来的同事问我,这些东西书上也没写啊。我说书上写的东西有时候跟不上真实场景,踩过坑才能记住。他又问那怎么判断该用哪种。我的答案是先写个性能测试,看看数据说话。
代码写久了会发现,很多性能问题不是什么高深的技术,就是这种基础操作没选对。花了三年才搞明白这个道理,希望你能比我早一点。