别再重复造轮子了!Python这10个内置模块真能造!
昨天晚上十一点多,我在公司楼下拿着一杯快凉了的奶茶,跟我们组那个小李闲聊,他一脸生无可恋地跟我说: “东哥,我又写了一个自己的日志工具。”
我当场就愣住了: “你写这玩意干嘛啊?logging 不香吗?”
他瞪我一眼:“我就记几行日志,还要学 logging?我自己 print 不行吗?”
我那一刻突然意识到,很多人写 Python,真的是在很用力地……重复造轮子。那些标准库里的模块,一个个被晾在那,简直委屈坏了。
我今天就边唠嗑边跟你说十个特别能打的内置模块,不是那种概念科普,而是你日常写脚本、做小工具、写业务代码时,直接能用上的那种,省时间、省 Bug,还显得自己很专业那种。你看着挑几个用就行,别全记,记住“先查标准库再写代码”这个习惯就够了。
先说那个最容易被鄙视、但是最该用的:logging
真事儿,我们之前线上的一个定时任务,偶尔会跑飞,但谁都不知道为啥,因为全是 print。print 有几个问题: 输出去哪儿不好控制、没有等级、没时间戳、重定向和格式全靠自己糊。
logging 基本一行配置,你的脚本立马从“学生作品”变“专业项目”,一点不夸张。你看个最小可用版:
import logginglogging.basicConfig(
level=logging.INFO, # 日志等级
format="%(asctime)s [%(levelname)s] %(message)s",
filename="app.log", # 写文件
filemode="a", # 追加写
)
logging.info("服务启动了")
logging.warning("磁盘快满了")
logging.error("出错了,赶紧看日志")
你平时写的小脚本,比如爬虫、同步任务、定时清理,只要把 print 换成 logging.xxx,以后查问题会舒服太多。 而且最关键一点:以后想把日志打到控制台、文件,甚至远程日志系统,配置层改一改就好,不用把代码翻个底朝天。
第二个,我特别想安利的是 pathlib,别再满世界 os.path.join 那样拼路径了,真的太丑
那天我在工位上看小李写一个整理下载目录的脚本,这样写的:
import os
import shutildownload_dir = "C:\\Users\\xxx\\Downloads"
for name in os.listdir(download_dir):
full_path = os.path.join(download_dir, name)
if os.path.isfile(full_path) and name.endswith(".pdf"):
target = os.path.join(download_dir, "pdf", name)
os.makedirs(os.path.dirname(target), exist_ok=True)
shutil.move(full_path, target)
我看着脑壳疼,Windows、Linux 换来换去就出问题。 完全可以用 pathlib 写得清楚很多:
from pathlib import Path
import shutildownload_dir = Path.home() / "Downloads"
for path in download_dir.iterdir():
if path.is_file() and path.suffix == ".pdf":
target_dir = download_dir / "pdf"
target_dir.mkdir(exist_ok=True)
shutil.move(str(path), str(target_dir / path.name))
你看,用 / 拼路径的感觉是不是顺眼多了? 而且像扩展名 suffix、文件名 name 这些常见操作全给你准备好了,根本不用自己切字符串。
说到 pathlib,顺手就得把 shutil 提一下,两兄弟搭配起来基本能搞定 80% 文件操作需求
比如你想做个“小型备份工具”,把某个目录里最近 7 天修改的文件打个压缩包丢到别处,这种小需求真特别适合 Python 处理。
from pathlib import Path
from datetime import datetime, timedelta
import shutilsrc = Path("/data/project")
backup_root = Path("/data/backup")
backup_root.mkdir(exist_ok=True)
cutoff = datetime.now() - timedelta(days=7)
tmp_dir = backup_root / "tmp"
tmp_dir.mkdir(exist_ok=True)
for path in src.rglob("*"):
ifnot path.is_file():
continue
mtime = datetime.fromtimestamp(path.stat().st_mtime)
if mtime >= cutoff:
rel = path.relative_to(src)
dest = tmp_dir / rel
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, dest)
archive_path = shutil.make_archive(
base_name=str(backup_root / datetime.now().strftime("%Y%m%d_backup")),
format="zip",
root_dir=str(tmp_dir),
)
shutil.rmtree(tmp_dir)
print("已生成备份:", archive_path)
你看:复制、保持元数据、打包、删临时目录,全是现成函数。你要是手写 zip 打包、手写递归复制,真就是在造轮子了。
第三个,我是真心希望所有写脚本的人,都早点学会 argparse,别再写那种每次都改源码传参数的脚本
你肯定见过这种:
# 顶上写死
INPUT_DIR = "/data/input"
DRY_RUN = False
每次想改参数,要么改代码重新部署,要么手动在命令行里改一堆环境变量,烦不烦。argparse 可以让你五分钟收获一个“像样的命令行工具”。
比如写个批量重命名脚本,支持 dry-run 预览:
import argparse
from pathlib import Pathdefmain():
parser = argparse.ArgumentParser(description="批量重命名文件")
parser.add_argument("path", help="目标目录")
parser.add_argument("--prefix", default="img_", help="文件前缀")
parser.add_argument("--dry-run", action="store_true", help="只打印,不真的改名")
args = parser.parse_args()
base = Path(args.path)
for i, f in enumerate(sorted(base.glob("*")), start=1):
ifnot f.is_file():
continue
new_name = f"{args.prefix}{i}{f.suffix}"
target = f.with_name(new_name)
if args.dry_run:
print(f"[预览] {f.name} -> {target.name}")
else:
f.rename(target)
print(f"[重命名] {f.name} -> {target.name}")
if __name__ == "__main__":
main()
你看着这个脚本,用起来就很自然:
python rename.py ./photos --prefix=holiday_ --dry-run
这已经很接近日常命令行工具体验了,而且你啥也没自己造,全是标准库。
说完命令行,得聊聊“偷懒神器”functools,这个模块听上去挺学术,但用起来其实就是“帮你少写一点重复代码”
最常见就是 lru_cache,缓存函数结果,特别适合那种“参数少、结果可复用”的函数,比如远程接口、数据库查询、复杂计算之类。
比如你有个取汇率的函数:
import functools
import time
import random@functools.lru_cache(maxsize=128)
defget_rate(currency: str) -> float:
print("真正访问远程服务...", currency)
time.sleep(1) # 模拟网络延迟
return7.0 + random.random()
print(get_rate("USD"))
print(get_rate("USD"))
print(get_rate("EUR"))
print(get_rate("USD"))
print(get_rate.cache_info())
你跑一下就知道了,同样的参数只有第一次会“慢吞吞”,后面直接走缓存。 很多人为了搞缓存,会整一堆 dict 做手动缓存、自己做过期管理,实际上有挺多场景直接用 lru_cache 就够了。
functools.partial 也很好用,比如你有一堆地方都要 json.dumps(..., ensure_ascii=False, indent=2),每次写一长串难看,可以直接:
import json
from functools import partialpretty_json = partial(json.dumps, ensure_ascii=False, indent=2)
data = {"name": "东哥", "skills": ["Python", "折腾"]}
print(pretty_json(data))
其实你可以把它理解成“给函数提前填好一部分参数,变成一个新函数”,在回调、排序、重复配置里特别实用。
第几个了我都数乱了,反正下面这个 itertools 我真的是越用越上头
一开始我也觉得这个模块有点“数学味儿”,什么排列组合、无限序列,感觉跟业务没啥关系。结果后来统计日志、拆批、滑动窗口,这些小需求,itertools 一出手就特别优雅。
举个特别实用的:把一个很长的列表拆成固定大小的小块,常常用在“批量写数据库”、“批量调用接口”里。很多人这么写:
defchunk(lst, size):
for i in range(0, len(lst), size):
yield lst[i:i+size]
其实可以直接用 itertools.islice 版本,适配任何迭代器,不要求有长度的那种:
from itertools import islicedefchunk_iter(iterable, size):
it = iter(iterable)
whileTrue:
batch = list(islice(it, size))
ifnot batch:
break
yield batch
nums = range(1, 23)
for batch in chunk_iter(nums, 5):
print(batch)
还有像 groupby,你做日志分组、按日期分bucket,写几行就搞定。 这个模块属于那种“你今天可能用不上,但你知道它存在,以后查一查文档就会发现惊喜”的那种。
再说一个大家更熟,实际却经常用残了的:collections
你每天写 dict 写得飞起,其实很多场景都适合它提供的那些“加强版容器”。举两个特别高频:Counter 和 defaultdict。
之前我们做一个简单的统计:算访问日志里每个用户出现多少次。 最原始写法是这样的:
counts = {}
for user_id in user_list:
if user_id in counts:
counts[user_id] += 1
else:
counts[user_id] = 1
换成 Counter :
from collections import Countercounts = Counter(user_list)
print(counts.most_common(10)) # 取 Top10
整个人都轻松了不少。defaultdict 就更实用了,任何“按 key 分组列表”的场景都能用,比如按部门把用户分组:
from collections import defaultdictusers = [
{"name": "小李", "dept": "研发"},
{"name": "小王", "dept": "研发"},
{"name": "小张", "dept": "运营"},
]
by_dept = defaultdict(list)
for u in users:
by_dept[u["dept"]].append(u["name"])
print(by_dept)
这俩东西基本能把你大部分 if/else 初始化 dict 的代码干掉。
稍微往“写业务系统”的方向走一步,dataclasses 真的是个救命模块
以前在 Python 里写个“数据类”,一般要这样:写 __init__、写 __repr__、写 __eq__,又长又烦。现在一个装饰器就完事:
from dataclasses import dataclass
from datetime import datetime@dataclass
classUser:
id: int
name: str
is_active: bool = True
created_at: datetime = datetime.now()
u = User(id=1, name="东哥")
print(u)
dataclass 会自动帮你生成构造函数、可读的 repr、比较方法等,你要是愿意,还可以 order=True 自动加排序。 这个东西跟 ORM、配置解析、接口返回对象验证都挺搭的,属于“让你的代码从字典地狱里爬出来”的那种。
第八个想说 datetime,不是说你会 datetime.now() 就算会了,时间这东西在业务里简直是雷区
最起码你要习惯“拿到的是 UTC,展示的是本地”,还有“计算间隔用 timedelta”,而不是自己写那些 + 86400 的魔法常量。
比如你做一个“最近七天活跃用户”的筛选:
from datetime import datetime, timedeltadefis_active_recently(last_login: datetime, days: int = 7) -> bool:
cutoff = datetime.now() - timedelta(days=days)
return last_login >= cutoff
要算两个时间点相差多少分钟,也不要搞字符串:
start = datetime(2024, 1, 1, 10, 0, 0)
end = datetime(2024, 1, 1, 11, 45, 0)
delta = end - start
print(delta.total_seconds() / 60, "分钟")
如果你的项目有跨时区需求,可以顺带了解下 zoneinfo(3.9+ 才有),你会发现“别的语言要装第三方库”的东西,Python 里经常自带了。
第九个,concurrent.futures,这个是很多人明明有“并发需求”,却只会在 for 循环里一个一个慢慢跑的原因
你想象一个场景:你要对一堆 URL 做请求、或对一堆文件做计算,可以并发处理,但又不想研究什么线程、进程的细节,这时候就特别适合用这个模块。
比如简单的多线程爬网页标题(就示意一下,不要拿去压生产啊):
from concurrent.futures import ThreadPoolExecutor, as_completed
import requestsurls = [
"https://www.python.org",
"https://www.baidu.com",
"https://www.qq.com",
]
deffetch_title(url: str) -> tuple[str, str]:
resp = requests.get(url, timeout=5)
text = resp.text.lower()
start = text.find("<title>")
end = text.find("</title>")
title = text[start + 7:end].strip() if start != -1and end != -1else"N/A"
return url, title
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_url = {executor.submit(fetch_title, u): u for u in urls}
for future in as_completed(future_to_url):
url, title = future.result()
print(url, "=>", title)
进程池同理,改成 ProcessPoolExecutor 就行,CPU 密集型任务(比如图片处理、加解密)会收益更明显。 很多人一提并发就想起 asyncio,但对大量“IO 密集但简单”的任务,用 futures 模块已经很够用了。
最后再补一个,json + typing 算半个组合技吧,这俩一个负责“数据在程序和外部之间跑来跑去”,一个负责“人在脑子里别搞混类型”
你平时写接口对接,经常是这样的:
import jsonraw = '{"id": 1, "name": "东哥", "tags": ["python", "后端"]}'
data = json.loads(raw)
print(data["name"])
这个没问题,但随着项目变大、字段变多,你完全靠惯性记住每个 key 是啥,很容易出错。搭配 typing.TypedDict 或 dataclasses,会舒服很多,IDE 也能帮你补全:
from typing import TypedDict, List
import jsonclassUser(TypedDict):
id: int
name: str
tags: List[str]
raw = '{"id": 1, "name": "东哥", "tags": ["python", "后端"]}'
data: User = json.loads(raw)
defgreet(u: User) -> None:
print("你好,", u["name"])
greet(data)
不夸张说,你给未来的自己省掉的时间,远远大于你现在多敲这几行注解。
你看,我绕来绕去说了一堆名字,其实就想表达一句话:很多时候你碰到的需求,Python 标准库早就帮你造好轮子了。
日志别自己瞎封装,先想想 logging; 文件和路径别 string 拼,看看 pathlib、shutil; 脚本别写死配置,摸摸 argparse; 想偷懒就翻翻 functools、itertools、collections; 写业务对象的时候想想 dataclasses; 时间、并发、数据结构,这些“长在 Bug 上的地方”,标准库也都帮你填了不少坑。
你以后写代码,哪怕就多做一个小动作:写之前,先在搜索框里敲一下 “python xxx 标准库” 或者 “python xxx module”,十次里有七次会发现,轮子已经有人帮你造好了。
行了我不啰嗦了,我那杯奶茶好像放桌上忘喝了,我先去…