Python技术迷

告别臃肿代码!Python这6个隐藏功能让效率翻倍

你有没有这种感受啊,就是写着写着 Python,文件一不小心三四百行起步,函数里全是 if else、for while,滚轮一拉都看不到头,那种窒息感…

昨天晚上十一点多我还在公司,准备下楼买瓶可乐,我们组那个小李拎着台电脑追过来跟我说:“东哥你帮我看看,我这个脚本才做了两个需求就 500 行了,再加一个功能我都不敢下手……”我随手扫了一眼,他那个代码典型的“能跑就行版”,Python 明明有很优雅的用法,他愣是写出了 C 风格。

我就顺手给他改了几段,小伙子当场惊呼:“卧槽,原来 Python 还能这么写?!” 今天正好有空,我就把那天给他讲的 6 个“小隐藏功能”整理一下,咱们就当闲聊,你看着挑自己顺手的用就行。

先说第一个小功能:拆包赋值,真的别再到处写 temp 了

大部分人第一次学 Python 交换两个变量值,都是这么干的:

a = 10
b = 20

temp = a
a = b
b = temp
print(a, b)  # 20 10

这不丑,但是很“非 Python”。Python 自带的拆包赋值,其实一个逗号就解决了:

a = 10
b = 20

a, 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 = age

def__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 或者专门聊生成器,我们再慢慢唠。