Python 写久了以后,有个挺明显的现象。 代码能写,需求也能做,但翻来覆去还是那几个东西:
for
if
dict
list
try
except
碰到批量处理,就手搓循环;碰到字典取值,就一层层判断;碰到资源释放,继续 try...finally。
代码当然没错。
但有些代码我现在回头看,第一反应就是:当时写得有点笨。
Python 其实藏了不少不算冷门、但很多人一直没真正用起来的特性。下面这 13 个,我挑的都是工作里真能塞进去的,不搞那些为了炫技才写出来的语法。
先看一个我现在挺常用的东西:enumerate()。
以前处理导入文件,经常能看到这种代码:
row_no = 0
for row in rows:
row_no += 1
if not row.get("phone"):
bad_rows.append({
"row": row_no,
"reason": "phone empty"
})
自己维护下标没什么必要。
for row_no, row in enumerate(rows, start=1):
if not row.get("phone"):
bad_rows.append({
"row": row_no,
"reason": "phone empty"
})
start=1 这个参数尤其适合 Excel、CSV 这种场景。
业务人员说“第 37 行数据有问题”,你日志里最好也打印第 37 行,别打印个数组下标 36,然后两边互相怀疑。
再比如 zip()。
有次处理一批接口返回字段,需要把字段名和结果拼起来。
笨写法一般这样:
payload = {}
for i in range(len(columns)):
payload[columns[i]] = values[i]
我现在一般直接:
payload = dict(zip(columns, values))
当然,这地方我会多看一眼两个列表长度。
Python 3.10 以后,zip() 可以直接加 strict=True:
payload = dict(zip(columns, values, strict=True))
长度对不上,直接报错。
这个比静默截断靠谱多了。
尤其是批量同步字段这种代码,静默少一个值,比直接挂掉更难查。
第三个是字典的 setdefault()。
做日志统计时,经常需要按照接口分组:
stats = {}
for item in access_logs:
uri = item["uri"]
if uri not in stats:
stats[uri] = []
stats[uri].append(item["cost"])
可以压成:
stats = {}
for item in access_logs:
stats.setdefault(item["uri"], []).append(item["cost"])
不过这里顺手说一句。
如果逻辑复杂,我反而更喜欢 defaultdict:
from collections import defaultdict
cost_bucket = defaultdict(list)
for item in access_logs:
cost_bucket[item["uri"]].append(item["cost"])
别为了少两行代码硬塞 setdefault()。
代码短不等于代码好看。
第四个是 collections.Counter。
我查日志的时候用得不少。
比如已经把一分钟内的错误日志提出来了,想看哪个错误最多:
errors = [
"timeout",
"timeout",
"connection reset",
"timeout",
"bad gateway",
"connection reset",
]
不用自己写字典计数:
from collections import Counter
count = Counter(errors)
print(count.most_common(3))
结果类似:
[
("timeout", 3),
("connection reset", 2),
("bad gateway", 1)
]
线上排障的时候,这种东西比肉眼翻几千行日志省事。
我甚至会直接把网关日志里的 URI、状态码、异常类型各跑一次 Counter,先看分布,再决定往哪个方向查。
第五个是 any() 和 all()。
这两个函数看着简单,但不少业务代码还在手动维护布尔变量。
比如批量导入用户,只要有一条手机号为空就拒绝:
invalid = False
for row in rows:
if not row.get("phone"):
invalid = True
break
if invalid:
raise ValueError("存在空手机号")
直接:
if any(not row.get("phone") for row in rows):
raise ValueError("存在空手机号")
反过来,如果要求全部任务都执行成功:
if all(task["status"] == "DONE" for task in tasks):
mark_batch_finished(batch_id)
这种代码读起来更接近业务本身。
我看代码时比较烦这种东西:
flag = True
然后往下翻几十行,途中被改三四次。
最后你还得重新推一遍这个 flag 到底代表什么。
第六个,next()。
查列表里的第一个匹配项,很多人还是这么写:
target = None
for job in jobs:
if job["job_id"] == wanted_id:
target = job
break
可以直接:
target = next(
(job for job in jobs if job["job_id"] == wanted_id),
None
)
这个写法我经常用在接口返回结果里找某条数据。
但别写成一条两百字符的大长句。
条件复杂了,老老实实拆开。
第七个是 for...else。
这个语法不少 Python 开发都见过,但真用过的人没想象中多。
比如查一批机器,找到异常节点就退出:
for node in nodes:
if node["load"] > 0.9:
print("找到高负载节点:", node["host"])
break
else:
print("本轮没有发现高负载节点")
注意这个 else 不是跟 if 配的。
它的意思是:
循环没有被 break 打断时,执行 else。
这个用来写搜索、校验挺顺手。
比如:
for row in rows:
if row["order_no"] == order_no:
handle(row)
break
else:
raise LookupError(f"order not found: {order_no}")
第一次看到确实有点别扭。
但在这种“找到了就 break,没找到就处理”的代码里,比额外维护一个 found 变量干净。
第八个是解包。
这个其实远不止:
a, b = b, a
我更常用的是星号解包。
比如接口路径:
parts = [
"api",
"v2",
"orders",
"83921"
]
可以:
root, version, *resource = parts
print(root)
print(version)
print(resource)
还有处理 CSV:
user_id, name, *extra = row
后面字段再增加几个,也不至于马上把解析代码干崩。
函数传参同样能解包:
query = {
"user_id": 1024,
"status": "ACTIVE"
}
load_users(**query)
配置驱动型代码里挺好用。
第九个是字典合并运算符 |。
以前合并配置经常这么写:
config = default_config.copy()
config.update(env_config)
config.update(user_config)
现在可以直接:
config = default_config | env_config | user_config
后面的值覆盖前面的值。
比如:
base = {
"timeout": 3,
"retry": 2,
"verify_ssl": True
}
prod = {
"timeout": 8,
"retry": 4
}
config = base | prod
这种特别适合配置覆盖。
我一般会按照:
默认配置
环境配置
运行参数
这个顺序合并。
顺序别写反。
这类 Bug 最恶心的地方就是代码完全不报错,只是你以为配置生效了,其实被后面覆盖了。
第十个是海象运算符 :=。
这个符号第一次看到,很容易觉得 Python 开始往奇怪方向发展了。
但用对地方其实挺舒服。
比如循环读取文件:
while chunk := file.read(8192):
process(chunk)
不用再写:
while True:
chunk = file.read(8192)
if not chunk:
break
process(chunk)
再比如处理接口结果:
if token := response.get("next_token"):
fetch_next_page(token)
不过这个东西很容易被玩坏。
像这种我就不喜欢:
if (data := get_data()) and (count := len(data)) > 10:
...
再继续套两层,代码基本就开始往谜语方向走了。
海象运算符适合减少一次无意义赋值,不适合拿来证明自己会 Python。
第十一个是 functools.lru_cache。
有些函数参数一样,结果也一样,但计算又比较贵,可以直接缓存。
比如读取某个固定服务的配置:
from functools import lru_cache
@lru_cache(maxsize=128)
def load_service_rule(service_name):
return parse_rule_file(
f"/etc/app/rules/{service_name}.json"
)
第一次调用会真正读取和解析。
后面相同参数直接走缓存。
我用这个时有个习惯:先确认函数是不是“适合缓存”。
如果函数里面查的是实时数据库:
@lru_cache
def get_order_status(order_id):
...
这种我第一眼就不太信。
订单状态都变了,你还把旧结果缓存函数里,后面排查起来够喝一壶。
缓存不是加个装饰器就完事,先看数据生命周期。
第十二个是 contextlib。
大家熟悉:
with open(...) as f:
其实自己的资源也可以做成 with。
比如临时切换工作目录:
from contextlib import contextmanager
from pathlib import Path
import os
@contextmanager
def workdir(path):
old = Path.cwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old)
使用:
with workdir("/data/import"):
run_import()
我比较喜欢这种写法的原因,不是少代码。
而是“清理动作”被固定住了。
文件关闭、锁释放、临时目录清理、数据库连接回收,这种东西最怕业务中途 return,或者异常后漏执行。
能塞进上下文管理器的,我一般不靠开发人员“记得释放”。
最后一个,match...case。
Python 3.10 之后支持结构化模式匹配。
如果只是这种代码:
if status == 1:
...
elif status == 2:
...
我觉得没必要为了新语法硬改。
它真正好用的是拆结构。
比如消息消费:
def handle_event(event):
match event:
case {
"type": "order_paid",
"order_id": order_id,
"amount": amount
}:
mark_paid(order_id, amount)
case {
"type": "order_cancelled",
"order_id": order_id
}:
cancel_order(order_id)
case {
"type": "inventory_changed",
"sku": sku,
"delta": delta
}:
change_stock(sku, delta)
case _:
raise ValueError(
f"unsupported event: {event}"
)
这种代码比连续写一堆:
event.get(...)
舒服很多。
特别是 MQ、Webhook、协议解析这种场景,本来就在根据消息结构分流,match 放进去还挺顺。
但我还是那个习惯:语法可以新,判断不能绕。
代码最后是给人维护的,不是给 Python 解释器欣赏的。
这 13 个东西里,我平时用得最多的其实不是最花哨的 match 和海象运算符,反而是:
enumerate、zip、Counter、any/all、解包、上下文管理器。
原因也简单。
它们解决的都是每天会碰到的小破事。
批量数据怎么带行号,两个列表怎么安全拼接,日志怎么快速统计,资源怎么保证释放,配置怎么覆盖。
Python 真正舒服的地方,不是能把十行代码压成一行。
而是有些你原本需要自己维护的状态,它已经帮你处理掉了。
这种代码我现在看到:
index = 0
found = False
result = {}
后面再跟几个循环,我一般都会多看两眼。
不是一定写错了。
只是八成还有地方能收拾。
以上就是“13 个可能未使用过的 Python 特性!”的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。
扫码二维码 获取免费视频学习资料

- 本文固定链接: http://www.phpxs.com/post/14614/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料