告别臃肿代码!Python这6个隐藏功能让效率翻倍
你有没有这种感受啊,就是写着写着 Python,文件一不小心三四百行起步,函数里全是 if else、for while,滚轮一拉都看不到头,那种窒息感…
昨天晚上十一点多我还在公司,准备下楼买瓶可乐,我们组那个小李拎着台电脑追过来跟我说:“东哥你帮我看看,我这个脚本才做了两个需求就 500 行了,再加一个功能我都不敢下手……”我随手扫了一眼,他那个代码典型的“能跑就行版”,Python 明明有很优雅的用法,他愣是写出了 C 风格。
我就顺手给他改了几段,小伙子当场惊呼:“卧槽,原来 Python 还能这么写?!” 今天正好有空,我就把那天给他讲的 6 个“小隐藏功能”整理一下,咱们就当闲聊,你看着挑自己顺手的用就行。
先说第一个小功能:拆包赋值,真的别再到处写 temp 了
大部分人第一次学 Python 交换两个变量值,都是这么干的:
a = 10
b = 20temp = a
a = b
b = temp
print(a, b) # 20 10
这不丑,但是很“非 Python”。Python 自带的拆包赋值,其实一个逗号就解决了:
a = 10
b = 20a, b = b, a
print(a, b) # 20 10
看着是不是舒服很多?
拆包不止能交换变量,你函数返回一堆东西的时候也特别好用:
defcalc(x, y):
return x + y, x - y, x * y# 拆成 3 个变量
s, diff, prod = calc(3, 5)
print(s, diff, prod)
# 或者只要其中几个
s, _, prod = calc(3, 5) # 用 _ 占位不需要的
print(s, prod)
还有一个经常能帮你“瘦身”的用法,就是从列表/元组中“拆前几个,剩下的全要”:
data = [1, 2, 3, 4, 5, 6]head, *middle, tail = data
print(head) # 1
print(middle) # [2, 3, 4, 5]
print(tail) # 6
小李原来有一堆代码是在取“第一个元素”和“剩下的元素”,自己写循环取的,看得我眼睛都疼,这种场景拆包直接用,代码量立马就下去了。
第二个小功能:列表推导式 / 字典推导式,for 循环减半那种
小李写过滤列表,原来是这么写的:
result = []
for x in nums:
if x % 2 == 0:
result.append(x * x)
没错,但很啰嗦。换成列表推导式就两行变一行,而且逻辑更集中:
result = [x * x for x in nums if x % 2 == 0]
你别小看这一行,很多业务代码里,大量“构建新列表”的逻辑,其实都可以合成一行推导式,文件行数真的是肉眼可见地在瘦。
再比如你从接口拿到一堆 dict,要以 id 当 key,原来写的:
users_map = {}
for u in users:
users_map[u["id"]] = u
推导式上:
users_map = {u["id"]: u for u in users}
顺手再举个套娃一点的例子,构造矩阵:
rows, cols = 3, 4
matrix = [[0for _ in range(cols)] for _ in range(rows)]
print(matrix) # 3x4 的全 0 矩阵
你要是还在 [[0] * cols] * rows 那么写,后面就会踩“引用同一行”的坑,推导式这种写法就没这个问题了。
第三个小功能:enumerate / zip,告别“手动数下标”
这个真的是面试最爱考,但是工作里一堆人不用。 小李当时有一段代码,长这样:
for i in range(len(items)):
print(i, items[i])
我问他:“你见过 enumerate 吗?” 他说:“见过,但我总记不住怎么用…”
事实上特别简单:
for idx, item in enumerate(items):
print(idx, item)
好处是啥? 一是少写一个 len 再去索引,二是 enumerate 本身还可以指定起始下标:
for idx, item in enumerate(items, start=1):
print(f"第 {idx} 个:", item)
zip 也是同理,两个列表一一配对,不用自己算长度、比较最小值:
names = ["小明", "小红", "小李"]
scores = [95, 88, 76]for name, score in zip(names, scores):
print(name, score)
甚至你可以搭配拆包玩点更花的,比如把二维列表“转置”一下:
data = [
[1, 2, 3],
[4, 5, 6],
]# 行列互换
transposed = list(zip(*data))
print(transposed) # [(1, 2), (3, 4)... 开个玩笑,实际是 (1, 4), (2, 5), (3, 6)]
那个星号解包在这里又出现了,拆包 + zip 这种组合,经常能把“手搓循环”的代码一刀砍掉好几行。
第四个小功能:with 上下文管理器,资源管理一把梭
还有一个典型“代码肥胖源头”就是各种 try/finally,手动关资源。 比如写文件:
f = open("data.txt", "w", encoding="utf-8")
try:
f.write("hello")
finally:
f.close()
你说麻烦不麻烦?with 一上:
with open("data.txt", "w", encoding="utf-8") as f:
f.write("hello")
缩进块结束就自动关闭了。 连异常都不用多想,出了问题也会帮你把文件关掉。
更好玩的是,你可以自己写上下文管理器,把一大坨“前后处理”的样板代码收进去。比如我那天给小李写了一个“简单打点”的东西,他调试接口老是手写 start/end 时间:
import time
from contextlib import contextmanager@contextmanager
deftimeit(name: str):
start = time.time()
try:
yield
finally:
cost = (time.time() - start) * 1000
print(f"[{name}] 耗时 {cost:.2f} ms")
# 用的时候:
with timeit("load_users"):
users = load_users_from_db()
# 这里随便写
原来他每个函数里都塞一对 start = time.time() 然后最后 print,复制粘贴的行数能占半屏,这么一包,整个项目清爽很多。
第五个小功能:生成器表达式 + any/all/sum,少造很多临时列表
有时候你只想“看一眼”有没有满足条件的元素,但手里习惯性就 new 一个列表,然后再去判断长度,这种也挺浪费的:
# 典型写法
evens = []
for x in nums:
if x % 2 == 0:
evens.append(x)has_even = len(evens) > 0
其实你根本不需要把它们都存下来,用生成器表达式配合 any 就行了:
has_even = any(x % 2 == 0for x in nums)
any 会遇到第一个 True 就停,不会把后面都算完。all 也类似,用来判断“是否所有都满足”:
all_positive = all(x > 0for x in nums)
再比如你想求和,但是中间又要做一点转换,其实也不用新建列表:
total = sum(price * count for price, count in items)
甚至 join 搭配生成器也是经典写法:
names = ["小明", "小红", "小李"]
s = ", ".join(name for name in names if name != "小李")
print(s)
小李原来这些地方都是先构建一个临时 list 再处理,长长一坨,我给他改成生成器之后,那文件直接少了几十行,而且内存占用也更省一点。
第六个小功能:dataclasses,告别又臭又长的“数据类模板”
这个我是真心推荐。很多同学写“数据对象”的时候,习惯性:
classUser:
def__init__(self, user_id, name, age):
self.user_id = user_id
self.name = name
self.age = agedef__repr__(self):
returnf"User({self.user_id}, {self.name}, {self.age})"
你看光是这三个字段,就要写一大截 self.xxx = xxx,字段再多一点,人都傻了。
Python 3.7 之后有了 dataclasses,专门干这种“数据载体”的活儿:
from dataclasses import dataclass@dataclass
classUser:
user_id: int
name: str
age: int = 18# 可以给默认值
u = User(1, "小明")
print(u) # User(user_id=1, name='小明', age=18)
print(u.name) # 小明
你不用写 __init__,不用写 __repr__,也不用手写比较方法。 要是需要不可变对象,顺手:
@dataclass(frozen=True)
classPoint:
x: int
y: int
frozen=True 会帮你做成“只读”的,创建完不能改属性,很多配置类、消息对象用这个就挺合适的。
我们当时项目里有一堆“DTO 类”是手写的,我花了一个晚上全给换成 dataclass,代码量直接砍掉一半,小李看 Git diff 都看傻了,以为我删错东西了。
顺带再提一句:这些功能拼一起,才是真的“瘦身”
上面说的 6 个,看着都是小技巧,但你一旦开始有意识地用,它们是可以组合拳的。 比如你写一个“读取文件 -> 解析 -> 过滤 -> 转对象”的流程,大概会长这样:
from dataclasses import dataclass
from pathlib import Path@dataclass
classRecord:
id: int
name: str
score: int
defload_records(path: str) -> list[Record]:
p = Path(path)
with p.open(encoding="utf-8") as f:
lines = (line.strip() for line in f)
# 跳过空行和注释
valid_lines = (ln for ln in lines if ln andnot ln.startswith("#"))
records = []
for idx, line in enumerate(valid_lines, start=1):
try:
sid, name, score = line.split(",")
records.append(
Record(int(sid), name, int(score))
)
except ValueError:
print(f"第 {idx} 行格式不对:{line!r}")
return records
if __name__ == "__main__":
rs = load_records("data.csv")
print(f"一共读到 {len(rs)} 条")
你数一下,这里其实已经用到了: 拆包、生成器表达式、enumerate、with、dataclass,还有一点点小的类型标注。 但整体看起来还挺自然的,不是那种“我为了用新语法而用”的感觉。
反正我自己的经验是啊,Python 真正拉开差距的,往往不是你会不会高级框架,而是你写的代码是不是“顺着语言来的”。 同样一个功能,C 风格写一遍、Python 风格写一遍,行数、可读性、维护成本完全两个世界。
你也不用一下子全记住,就找自己代码里最肉的一块,挑一两个点试着改改: 比如把手写 temp 的交换改成拆包,把 for+append 换成推导式,把一堆 try/finally 换成 with。 改着改着,你就会发现,那个动不动 500 行才写完俩功能的脚本,慢慢就变成了 100 来行的小而美工具。
行了我这边也差不多,楼下奶茶快打烊了,我先去买一杯,明天要是你们想看具体某一块再展开,比如专门聊 dataclass 或者专门聊生成器,我们再慢慢唠。