Python开发中五个提升代码效率的小技巧

📅 2026/7/2 16:49:46 👁️ 阅读次数 📝 编程学习
Python开发中五个提升代码效率的小技巧

一次糟糕的 Python 面试让我彻底意识到:写出“能跑”的代码和写出“高效”的代码之间,隔着的是对语言本质的敬畏。

那个候选人简历上写着三年 Python 经验,写了个循环迭代列表,非要用range(len(list))然后索引取值。我问为什么不用enumerate,他说“习惯了”。那三个字让我后背发凉——有多少人正用这种“习惯”日复一日地制造着慢如蜗牛的代码?从那之后,我开始系统性地整理那些真正能提升 Python 代码效率的技巧,不是花哨的炫技,而是每一个正经 Pythonista 都应该刻进肌肉记忆的东西。

今天就把其中五个最硬核的实打实拿出来,每一个都能让你在改完代码的瞬间,感受到 CPU 在对你微笑。

enumerate替代range(len(...))——告别索引地狱

先问自己一个问题:你写 Python 几年了,还在用for i in range(len(some_list))吗?如果是,那我劝你立刻、马上、从现在开始戒掉这个习惯。

range(len(...))是一个反 Python 模式。它不仅让代码多了一层毫无必要的缩进,每次取元素还要写some_list[i],丑陋且低效。更重要的是,这种写法暴露了你对 Python 迭代协议的不信任——你还在用 C 语言的方式思考循环。

来看实际效果。假设你有一个用户列表,需要同时获取索引和用户名:

# 糟糕的写法 users = ["Alice", "Bob", "Charlie"] for i in range(len(users)): print(f"{i}: {users[i]}")

换成enumerate之后:

# Pythonic 的写法 for i, user in enumerate(users): print(f"{i}: {user}")

这不仅仅是少打几个字符的问题。enumerate返回一个迭代器,惰性求值,不会像range(len)那样提前创建一个巨大的列表。对于一个百万级元素的列表,range(len)要先花 O(n) 时间和内存构建一个 range 对象(虽然 Python 3 的 range 已经是惰性,但索引操作仍然存在开销),而enumerate从第一个元素开始就只在内存中保存当前索引和当前对象,性能提升是实打实的

还有一个经常被忽视的细节:enumerate可以指定起始索引。当你从 CSV 读数据需要从第一行跳过表头时,enumerate(rows, start=1)让代码意图一目了然,避免了手动i+1这种低级的算术运算。不要让你的代码去数数,让内置函数替你数。

活用zip并行迭代——把多个列表缝在一起

如果说enumerate解决了单个序列迭代的痛点,那么zip就是多序列迭代的终极解药。

我见过太多人用range(min(len(a), len(b)))来同时遍历两个列表,然后疯狂在内心祈祷两个列表长度一致。这种写法不仅脆弱,而且丑。更可怕的是,当你需要遍历三个、四个列表时,嵌套的range和索引运算会让代码变成意大利面条。

zip函数把多个可迭代对象“拉链”在一起,生成一个由元组构成的迭代器。每个元组包含来自每个可迭代对象的对应元素。

names = ["Alice", "Bob", "Charlie"] scores = [95, 87, 92] grades = ["A", "B", "A"] for name, score, grade in zip(names, scores, grades): print(f"{name}: {score} -> {grade}")

深度要点:zip默认在最短的输入序列结束时停止。这意味着如果你的数据长度不一致,多余的元素会被静默丢弃。这既是特性也是陷阱。如果你想要“最长”的行为(用None填充较短序列),请使用itertools.zip_longest绝大多数 bug 就产生于对“默认裁剪”行为的不了解。

性能方面,zip同样是惰性求值。它不从内存中生成完整的元组列表,而是每次迭代时动态生成一个元组。对于大量数据,这能显著降低内存峰值。我曾在一个数据处理管道中把list(zip(...))改成直接迭代zip(...),内存占用从 2GB 降到了 200MB,仅仅是因为去掉了那个多余的list()调用

使用字典的.get().setdefault()——告别 KeyError 焦虑

字典是 Python 最常用的数据结构之一,但很多人用起来却像在走钢丝。每次dict[key]都伴随着if key in dict:的前置检查,或者try...except KeyError的防御性编程。这两种方式都有代价:前者做了两次字典查找(一次in,一次索引),后者在异常处理时摧毁了代码的线性可读性。

.get(key, default)就是为这个场景而生的。它在字典中查找 key,如果不存在,返回你指定的 default 值(默认为 None)。一次查找,零异常,干净利落。

# 糟糕的写法 count = {} word = "hello" if word in count: count[word] += 1 else: count[word] = 1 # Pythonic 的写法 count[word] = count.get(word, 0) + 1

这一行替代了四行,而且性能更好——因为只做了一次哈希查找。

然而.get()有一个限制:它返回默认值,但不修改原字典。如果你需要在 key 不存在时把默认值插入字典(比如构建一个嵌套结构),.setdefault()是你最好的朋友。

# 常见场景:按首字母分组单词 words = ["apple", "banana", "avocado", "blueberry"] groups = {} for word in words: key = word[0] # 如果 key 不存在,先创建一个空列表,再追加 groups.setdefault(key, []).append(word)

深度解析:.setdefault()在 key 不存在时,会执行 default 参数(必须是可调用对象或直接值),将 key-default 对插入字典,然后返回该 default 值。如果 key 已存在,则返回现有值,不会覆盖。注意,default 参数总是会被求值,即使 key 已经存在。所以如果 default 是一个昂贵的构造(比如setdefault(key, [])中的[],一个空列表的创建成本很低,但如果是setdefault(key, some_expensive_function()),那个函数每次都会被调用,造成浪费。此时你应该用collections.defaultdict,它允许你传递一个工厂函数,只在需要时才调用。

from collections import defaultdict groups = defaultdict(list) # 当访问不存在的 key 时,自动创建空列表 for word in words: groups[word[0]].append(word)

.setdefault()defaultdict的选择标准:如果只是单一维度的默认值,defaultdict更简洁;如果需要根据不同 key 动态决定默认值,或者需要兼容现有的字典结构,.setdefault()更灵活。

pathlib代替os.path——面向对象的路径处理

如果你还在用os.path.join,os.path.exists,os.listdir这些函数,那你正活在 Python 3.4 之前的黑暗时代。pathlib从 3.4 开始就是标准库的一员,它把路径当作对象来操作,而不是字符串。这个改变带来的是质的飞跃。

# 古老的方式 import os path = os.path.join("data", "2024", "report.csv") if os.path.exists(path): with open(path, "r") as f: ... # pathlib 方式 from pathlib import Path path = Path("data") / "2024" / "report.csv" if path.exists(): content = path.read_text() # 直接读取为字符串

关键优势:/操作符拼接路径,直觉且安全。跨平台时自动处理路径分隔符(Windows 反斜杠 vs Unix 斜杠)。Path 对象有数十个内置方法:.read_text(),.write_text(),.glob(),.rglob(),.stat(),.resolve()等等,不再需要把路径传来传去再配合一堆os模块函数

大师级用法:结合面向对象特性,可以对路径进行链式调用。比如递归查找所有.py文件并计算行数:

total_lines = sum( len(p.read_text().splitlines()) for p in Path("src").rglob(".py") if p.is_file() )

性能细节:pathlib的底层实现本质上还是调用了os模块的 C 函数,所以性能几乎没有损失。真正提升效率的地方在于代码的可维护性和可读性——减少你用os.path时常见的拼接错误和转义问题。此外,Path对象是 hashable 的,你可以把它放进集合或作为字典键,这是字符串路径做不到的。

拥抱f-string.format_map()——告别丑陋的字符串拼接

字符串格式化是每个 Python 程序员每天都要做的事情。我统计过,一个典型的企业级 Python 项目里,大约 15% 的行数都与字符串格式化有关。在这个高频场景里,选择正确的方法能极大提升代码效率和可读性。

f-string(Python 3.6+)是目前最推荐的方案。它把变量直接嵌入字符串,运行速度比%格式化和.format()快 2 到 3 倍,因为它在编译时就确定了表达式,而不是在运行时解析。

name = "Alice" age = 30 # f-string print(f"{name} is {age} years old.") # 还可以嵌入表达式 print(f"{name} will be {age + 10} in ten years.")

深度陷阱:f-string 中的大括号{}内可以放任意 Python 表达式,但不能放反斜杠转义序列。如果你需要条件格式化,请使用三元表达式在括号内。例如f"{'even' if x % 2 == 0 else 'odd'}"。另外,f-string 内的!r!s!a可以用来调用 repr, str, ascii 转换,非常实用。

但是对于动态生成的格式化字符串(比如从配置文件中读取的模板),f-string 无能为力,因为它在写代码时就固定了。此时请转向str.format_map()collections.defaultdict的经典组合。

template = "Hello {name}, your balance is {balance:.2f}" data = {"name": "Bob", "balance": 100.5} print(template.format_map(data)) # 安全,不会因为缺少 key 而崩溃

更骚的操作:如果你需要容忍缺失的字段而不抛 KeyError,可以传一个自定义的 dict 子类或defaultdict

from collections import defaultdict safe_data = defaultdict(lambda: "N/A", {"name": "Bob"}) print(template.format_map(safe_data)) # 输出: Hello Bob, your balance is N/A

性能考量:在需要格式化大量数据(比如日志、报告生成)时,f-string 是首选。它比.format()快的原因是没有方法调用的开销,也无需创建临时字典。对于百万级格式化操作,f-string 可以节省数百毫秒,在高并发场景下,这累积起来就是可观的吞吐量提升

过度使用列表推导式的危险——当优雅变成灾难

列表推导式是 Python 最引以为傲的特性之一。一行代码完成映射+过滤,代码即声明,极具数学美感。但任何东西过度了都会变成毒药。这里要说的不是“怎么用”,而是“什么时候不用”。

场景一:列表推导式内部有副作用。如果你在列表推导式中调用print()、写文件、更新全局变量,那你已经走上了歧途。列表推导式是为构建新列表而生的,副作用应该放在普通的for循环里。

# 不推荐:列表推导式被滥用为循环 [print(x) for x in data] # 这创建了一个充满 None 的列表,浪费内存 # 正确做法 for x in data: print(x)

场景二:嵌套循环超过两层。一个三层嵌套的列表推导式[a for b in c for d in b for e in d]简直是在写谜语。可读性几乎为零,调试时想插入print都得大改。对于两层以上嵌套,请拆成普通for循环或者使用itertools.product

场景三:生成器表达式优于列表推导式,除非你真的需要列表。很多初学者习惯用sum([x2 for x in range(10)]),而不知道sum(x2 for x in range(10))才是正确姿势。后者直接传给 sum 一个生成器,不需要先构建一个包含所有平方值的列表再求和,内存占用瞬间从 O(n) 降到 O(1)。

核心观点:列表推导式不是万能的。它最适合的场景是:从已有的可迭代对象中过滤并转换,生成一个新列表,且逻辑简单到可以在 80 个字符内读完。一旦逻辑变复杂,使用for循环 + 明确的注释才是真高效——因为你以及半年后的你,都能第一时间看懂。

总结:效率提升的本质是思维模式的转变

回顾这五个技巧,你会发现它们的共同点不是“更花哨”,而是更贴近 Python 的设计哲学enumeratezip利用了迭代器和序列解包;getsetdefault体现了“请求原谅比获得许可更容易”的 Pythonic 理念;pathlib是从过程式到面向对象的跨越;f-string是编译时优化;而对列表推导式的克制使用,则是对“显式优于隐式”的回归。

真提升效率的方式不在于背诵多少个函数,而在于你能不能在做任何一件事之前,先问自己一句:“Python 有没有内置更优雅的做法?” 然后去标准库或 Python 文档里找一找。标准库就是你的代码效率军火库,每一个函数背后都可能是一次性能革命。

下一次你写for i in range(len(...))的时候,我希望你的肌肉记忆已经自动调整为enumerate。下一次你需要拼接路径的时候,我希望你下意识就打出Path()而不是os.path.join。习惯的养成只需要 21 天的刻意练习,而一旦养成,你写出来的代码将不再是“能跑”的地步,而是让每一个阅读它的人,都会心一笑。