Python技术迷

别再重复造轮子了!Python这10个内置模块真能造!

昨天晚上十一点多,我在公司楼下拿着一杯快凉了的奶茶,跟我们组那个小李闲聊,他一脸生无可恋地跟我说: “东哥,我又写了一个自己的日志工具。”

我当场就愣住了: “你写这玩意干嘛啊?logging 不香吗?”

他瞪我一眼:“我就记几行日志,还要学 logging?我自己 print 不行吗?”

我那一刻突然意识到,很多人写 Python,真的是在很用力地……重复造轮子。那些标准库里的模块,一个个被晾在那,简直委屈坏了。

我今天就边唠嗑边跟你说十个特别能打的内置模块,不是那种概念科普,而是你日常写脚本、做小工具、写业务代码时,直接能用上的那种,省时间、省 Bug,还显得自己很专业那种。你看着挑几个用就行,别全记,记住“先查标准库再写代码”这个习惯就够了。

先说那个最容易被鄙视、但是最该用的:logging

真事儿,我们之前线上的一个定时任务,偶尔会跑飞,但谁都不知道为啥,因为全是 print。print 有几个问题: 输出去哪儿不好控制、没有等级、没时间戳、重定向和格式全靠自己糊。

logging 基本一行配置,你的脚本立马从“学生作品”变“专业项目”,一点不夸张。你看个最小可用版:

import logging

logging.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 shutil

download_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 shutil

download_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 shutil

src = 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 Path

defmain():
    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 partial

pretty_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 islice

defchunk_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 Counter

counts = Counter(user_list)
print(counts.most_common(10))  # 取 Top10

整个人都轻松了不少。defaultdict 就更实用了,任何“按 key 分组列表”的场景都能用,比如按部门把用户分组:

from collections import defaultdict

users = [
    {"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, timedelta

defis_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 requests

urls = [
"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 json

raw = '{"id": 1, "name": "东哥", "tags": ["python", "后端"]}'
data = json.loads(raw)
print(data["name"])

这个没问题,但随着项目变大、字段变多,你完全靠惯性记住每个 key 是啥,很容易出错。搭配 typing.TypedDict 或 dataclasses,会舒服很多,IDE 也能帮你补全:

from typing import TypedDict, List
import json

classUser(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”,十次里有七次会发现,轮子已经有人帮你造好了。

行了我不啰嗦了,我那杯奶茶好像放桌上忘喝了,我先去…