
那天服务器报警了周五下午三点半我正准备收拾东西下班。运维同事在群里发了一条消息配了一张截图“线上内存使用率95%你们谁在跑什么任务”我后背一凉。今天确实部署了一个新脚本用来处理上个月的用户行为日志。文件不大也就2.8GB。我心想用列表推导式把数据读进来过滤一下Python嘛写起来多优雅logs [line for line in open(user_logs.txt) if error in line]就这一行。优雅简洁Pythonic。然后内存就爆了。我盯着屏幕上那行代码不敢相信问题出在这里。2.8GB的文件这行代码直接给我干出了4.2GB的内存占用。服务器开始疯狂swap磁盘IO飙到100%整个服务都跟着抖动。一个小时后我把这行代码改成了一行生成器logs (line for line in open(user_logs.txt) if error in line)内存占用降到了不到50MB。速度反而还快了。那天我下班的时候天已经黑了。但有一件事我彻底搞明白了列表推导式和生成器表达式看起来就差一个括号实际上是天壤之别。列表推导式到底做了什么先来看看列表推导式的工作原理。当Python执行这行代码时squares [x**2 for x in range(10000000)]背后发生的是Python先创建一个空列表开始循环每次计算x**2把计算结果一个一个塞进列表里直到循环结束这个过程看起来没问题对不对问题就在于第3步——所有计算结果都被存进了内存。1000万个整数每个整数28个字节Python的int对象开销很大再加上列表本身的空间——280MB就这样没了。这就是列表推导式的“原罪”它一次性把所有结果全部生成并存储。数据量小的时候这根本不是问题。但数据量一大它就像一台内存抽水机把可用内存榨得干干净净。列表推导式的内存消耗有多夸张我做个测试给你看。读取一个1GB的日志文件过滤出包含ERROR的行# 列表推导式 lines [line for line in open(log.txt) if ERROR in line]运行过程中内存占用峰值是多少3.2GB。为什么会比文件本身还大因为Python把每一行都拆成了独立的字符串对象每个字符串都有额外的对象头开销大约49个字节。文件里的原始字节变成了Python对象内存占用直接膨胀了好几倍。如果用普通for循环加append结果是一样的lines [] for line in open(log.txt): if ERROR in line: lines.append(line)内存占用依然是3.2GB。列表推导式只是写法更简洁底层的本质没变——都是把结果全部装进列表。生成器到底做了什么再看生成器表达式的写法lines (line for line in open(log.txt) if ERROR in line)运行这一行内存占用几乎为0——准确地说几十KB。为什么因为这一行代码什么都没算。它只是创建了一个生成器对象。这个对象记住了三件事你要遍历哪个可迭代对象open(log.txt)你要执行什么操作line for line ... if ERROR in line现在遍历到什么位置了还没开始当你真正遍历它的时候for line in lines: process(line)生成器才开始工作读一行处理一行处理完就丢掉再读下一行。内存里始终只有一行数据。这就是生成器的核心思想惰性求值——需要的时候才计算算完就扔绝不囤货。不是说生成器一定更快这里要澄清一个常见的误解生成器不一定比列表推导式快。事实上在大多数场景下列表推导式更快。为什么因为列表推导式在C语言层面做了优化循环速度比Python的for循环要快。而且一次性分配好内存比不断地申请释放内存效率更高。我做了一个简单的性能测试场景1计算1000万个数的平方方式耗时内存占用列表推导式0.47秒280MB生成器表达式0.52秒几乎为0列表推导式快了大约**10%**。但场景2处理一个2.8GB的日志文件方式耗时内存占用结果列表推导式12.3秒4.2GB内存溢出生成器表达式11.8秒48MB正常完成看到区别没有当数据量小到内存装得下的时候列表推导式确实快。但当数据量大到内存装不下的时候——快有什么用直接崩了。所以选择的标准很简单数据量小内存随便装 → 列表推导式简洁又快数据量大内存吃紧 → 生成器保命要紧什么时候该用生成器判断标准就一条你是否需要一次性持有所有数据。不需要就用生成器。下面这些场景强烈建议用生成器1. 处理大文件# 错误写法 lines [line.strip() for line in open(huge_file.csv)] # 正确写法 lines (line.strip() for line in open(huge_file.csv))2. 数据流处理# 从API分页获取数据 def fetch_all_users(): page 1 while True: data requests.get(f/users?page{page}) if not data: break for user in data: yield user page 1 # 使用 for user in fetch_all_users(): process(user) # 边取边处理不会把所有用户都存内存里3. 无限序列# 生成无限斐波那契数列 def fibonacci(): a, b 0, 1 while True: yield a a, b b, a b # 只取前100个 for num in fibonacci(): if num 100: break print(num)如果用列表你根本没法表示无限序列——内存会炸。生成器不只有这一种写法除了生成器表达式圆括号那个Python还有两种方式创建生成器方式一yield关键字def read_large_file(path): with open(path) as f: for line in f: yield line # 每次返回一行任何包含yield的函数调用时返回的都是生成器对象。方式二生成器表达式squares (x**2 for x in range(1000000))就是前面讲的圆括号写法。两种方式选哪个逻辑简单用表达式逻辑复杂用函数。进阶链式生成器生成器还有一个特别酷的用法——链式组合。你可以把多个生成器串起来每个只做一件事清晰又高效def read_lines(path): with open(path) as f: for line in f: yield line def filter_error(lines): for line in lines: if ERROR in line: yield line def extract_timestamp(lines): for line in lines: yield line.split(|)[0] # 假设时间戳在第一列 # 组合使用 lines read_lines(log.txt) errors filter_error(lines) timestamps extract_timestamp(errors) for ts in timestamps: print(ts) # 每一步都是流式处理内存占用始终最小每一步都是惰性的数据像流水一样经过一个个处理环节内存里永远只有一条数据。还有两种推导式也同理列表推导式有内存问题那字典推导式和集合推导式呢一样的。# 字典推导式——一次性生成全部 user_map {u.id: u for u in get_all_users()} # 如果用户有百万级内存直接爆炸 # 改为生成器配合字典构造 user_map dict((u.id, u) for u in get_all_users()) # 稍微好一点但还是存了全部但注意字典和集合本质上就是要存全部数据的。如果你需要字典或集合那就没办法内存占用是必须的。这时候能优化的是数据源部分——用生成器逐个产出数据避免数据源本身再占一份内存。一个实战中的取舍回到开头那个故事。2.8GB的日志文件我后来是怎么处理的def process_logs(file_path): with open(file_path) as f: # 生成器表达式逐行读取 error_lines (line for line in f if ERROR in line) # 生成器函数解析每行 parsed (parse_line(line) for line in error_lines) # 又一个生成器只取需要的时间段 filtered (item for item in parsed if item[timestamp] cutoff) # 最后汇总统计 stats {} for item in filtered: stats[item[type]] stats.get(item[type], 0) 1 return stats整个流程中内存里最多同时存在几行数据。2.8GB的文件处理完内存峰值不到100MB。服务器稳了我也稳了。一句话总结列表推导式爽但贪婪。一次吃光所有数据。生成器表达式克制但持久。吃一口消化一口。数据小的时候随便你用哪个代码好看更重要。数据大的时候——用生成器是程序员最后的体面。别让你的服务器为一行代码殉情。