学习目标:建立"标准库即兵器库"的心智地图——知道什么场景该翻哪个模块;吃透最常用的十个工具箱(容器/迭代器/函数/时间/数字/进程/持久化/日志/类型)的底层实现与陷阱;能区分"用标准库"与"上第三方库"的边界,并学会直接阅读
Lib/源码。
前 10 章把 Python 语言本身讲透了:对象模型、控制流、函数、类、IO、模块。但"会语言"不等于"会干活"——真正让你写程序快的是标准库。Python 的杀手锏之一就是"自带电池"(batteries included):tar 解压、Excel 读取、数据库、HTTP 服务器……系统该配的都配齐了。本章不是 API 字典,而是兵器库导览:每个模块讲清"它解决什么问题、底层怎么实现、坑在哪、什么时候该换第三方"。
本章与前面章节的关系:第 3 章讲了 dict/list/tuple 等内置容器,13.2 的 collections 是它们的专业变体;第 5 章的迭代器协议与生成器,在 13.3 的 itertools 里被用到极致;第 6 章的函数式编程(lambda/高阶函数/闭包),13.4 的 functools 是它的标准库化;第 9 章的 json/pathlib/tempfile/struct 与第 7 章的 dataclass/enum、第 8 章的 contextlib、正则专题的 re,本章均不重复,只在需要时交叉引用;queue/threading 等并发工具点到为止,第 11 章细讲。
Python 标准库有 300+ 个模块条目(sys.stdlib_module_names 数到 313),从 abc(抽象基类)到 zoneinfo(时区),大部分是纯 Python(在 Lib/ 目录),少部分是 C 实现(在 Modules/ 目录,编译进解释器或作为内置扩展)。
# 标准库全量清单(3.10+ 提供)
>>> import sys
>>> len(sys.stdlib_module_names)
313
>>> "collections" in sys.stdlib_module_names
True
>>> "requests" in sys.stdlib_module_names # 第三方不在其中
Falsesys.stdlib_module_names(PEP 594 时代的配套设施)是"标准库模块的权威清单"。注意:os.path 这类子模块不算独立条目,且清单不含 __main__ 等运行时伪模块。
按领域把常用模块画成"六象限地图":
| 领域 | 代表模块 | 本章位置 |
|---|---|---|
| 数据与容器 | collections、itertools、functools、array、bisect、heapq |
13.2–13.4 |
| 文本与时间 | string、datetime、zoneinfo、calendar、unicodedata |
13.5 |
| 数字与随机 | math、decimal、fractions、random、statistics |
13.6 |
| 系统与进程 | os、sys、subprocess、shutil、glob、platform |
13.7 |
| 持久化与配置 | sqlite3、csv、configparser、argparse、json(第9章) |
13.8 |
| 工程工具 | logging、typing、dataclasses(第7章)、contextlib(第8章) |
13.9–13.10 |
实战建议:不要背清单——按问题找模块:遇到"去重计数"查
collections.Counter,遇到"嵌套循环"想itertools.product,遇到"进程调用"用subprocess.run。本章给你的就是这个"问题 → 模块"的索引。
六象限地图里"文本与时间"横跨第 13.5 与这里。除 str 方法(第 3 章)与 re(专题)外,三个高频文本工具:
>>> import string
>>> string.ascii_letters, string.digits # 字符常量集
('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', '0123456789')
>>> string.Template("$name is $age").substitute(name="Alice", age=30) # 模板替换
'Alice is 30'
>>> # Template 的 $ 语法与 str.format 的 {} 语法对比:模板适合"非程序员编辑"的场景
>>> import unicodedata
>>> unicodedata.name("汉") # 码点 → 名称
'CJK UNIFIED IDEOGRAPH-6C49'
>>> unicodedata.normalize("NFKC", "fullwidth") # Unicode 规范化(全角→半角)
'fullwidth'
>>> import textwrap
>>> textwrap.wrap("很长的文本……", width=20) # 按宽度折行
['很长的文本……', ...]
>>> textwrap.dedent("""\
... indent 对齐
... dedent 移除公共前导空白""")
' indent 对齐\ndedent 移除公共前导空白'实战建议:
string.Template用于"用户可编辑的模板"($var语法比{}更不易出错);unicodedata.normalize是文本清洗的起点(全半角、组合字符);textwrap.dedent搭配多行字符串写"缩进安全的 docstring/代码生成"。codecs/bytes解码细节已在第 9 章覆盖。
"标准库一定够用"和"标准库一定落后"都是错的。决策矩阵:
| 维度 | 标准库 | 第三方 |
|---|---|---|
| 稳定性 | ★★★★★(版本演进极其保守) | 参差(看维护状况) |
| 依赖成本 | 零(自带) | 要装、要锁、要升级 |
| 性能 | 核心模块是 C 实现,够快 | 优化空间大(如 orjson) |
| 功能覆盖 | 通用,不追新 | 领域专用、迭代快 |
| 风险 | 演进慢(想换 API 要等大版本) | 供应链、停止维护、API 漂移 |
经典案例:urllib vs requests
# 标准库 urllib:能用,但 API 反人类
import urllib.request
with urllib.request.urlopen("https://api.example.com/data", timeout=10) as resp:
data = resp.read()
# 第三方 requests:20 行变 1 行,且自动处理连接池/重试/编码
import requests
resp = requests.get("https://api.example.com/data", timeout=10)
data = resp.json()urllib 是标准库,requests 是第三方——但工程上几乎总是选 requests。为什么?因为 HTTP 客户端的痛点不在"能不能发请求",而在连接池、重试、超时、编码探测、会话 cookie 这些工程细节,标准库的定位是"协议实现"而非"好用客户端"。
设计哲学:标准库承诺的是"正确的基础能力"而不是"最好的体验"。判断标准:你的场景是"偶尔用一次的系统能力"(标准库够)还是"天天用的核心路径"(值得上成熟的第三方)?另一个信号:标准库新版本也会吸收社区最佳实践——
zoneinfo(PEP 615)吸收pytz的教训、importlib.metadata吸收pkg_resources的教训、dataclasses吸收attrs的教训。每 2–3 年回看一次标准库,新版本可能已经替你升级了。
"读标准库源码"是 Python 工程师的最高效成长路径——它代表官方认可的写法,且多数模块就是普通 Python。
# 方法 1:inspect 直接看源码(无需找路径)
>>> import inspect, collections
>>> print(inspect.getsource(collections.Counter.most_common))
def most_common(self, n=None):
'''List the n most common elements...'''
if n is None:
return sorted(self.items(), key=_itemgetter(1), reverse=True)
return heapq.nlargest(n, self.items(), key=_itemgetter(1))# 方法 2:直接翻 Lib/ 目录(Windows 上在解释器目录下)
$ python -c "import sysconfig; print(sysconfig.get_path('stdlib'))"
C:\Python314\Lib# 方法 3:区分"纯 Python 实现"与"C 实现"
>>> import collections, json
>>> collections.Counter.__module__ # 顶层类是 C 实现
'_collections' # ← C 加速部分在 _collections
>>> json.JSONDecoder.__module__ # 纯 Python
'json.decoder'🔑 机制洞察:标准库的常见模式是"C 加速 + Python 兜底"——
collections的_collections(C)与纯 Python 版本并存,导入时优先 C(_collections存在则用,否则退回_collections_abc的纯 Python 实现)。import collections; collections.Counter.__module__显示_collections正是这个机制的证据。读源码时先问一句"这是 C 还是 Python 实现",能避免把"实现细节"当成"语言特性"。
第 3 章的内置容器(dict/list/tuple/set)是地基,collections 是"专业变体"——每个都是为解决一个具体痛点而生的。
>>> from collections import defaultdict
>>> d = defaultdict(list)
>>> d["key"] # 访问不存在的键 → 不抛 KeyError,调用 list() 生成默认值
[]
>>> d["key"].append(1) # 经典场景:分组收集
>>> d
defaultdict(<class 'list'>, {'key': [1]})机制:defaultdict 是 dict 子类,重写了 __missing__(key)——dict.__getitem__ 在键不存在时会调用它(第 7 章 7.6 的属性查找是另一处 __missing__ 之外的钩子,这里才是 __missing__ 的正主):
# defaultdict 的核心逻辑(collections/__init__.py 中的简化版)
class defaultdict(dict):
def __missing__(self, key):
if self.default_factory is None:
raise KeyError(key)
self[key] = self.default_factory() # 生成默认值并写入
return self[key]# ❌ 反模式:手动检查键存在性(啰嗦且容易漏)
d = {}
if "key" not in d:
d["key"] = []
d["key"].append(1)
# ✅ defaultdict:一行
d = defaultdict(list)
d["key"].append(1)
⚠️ 陷阱:defaultdict的"默认值"只在d[key]访问时触发——d.get(key)、key in d、d.setdefault(key)不触发__missing__。setdefault与defaultdict的差异要分清:前者每次调用都执行默认值工厂(即使键已存在),后者只在缺失时执行一次。
>>> from collections import Counter
>>> c = Counter("abracadabra")
>>> c
Counter({'a': 5, 'b': 2, 'r': 2, 'd': 1, 'c': 1})
>>> c.most_common(2)
[('a', 5), ('b', 2)]Counter 是 dict 子类(键 → 计数),但附加了计数的算术语义:
>>> Counter("aab") + Counter("abb") # 加法:计数相加
Counter({'a': 2, 'b': 3})
>>> Counter("aab") - Counter("abb") # 减法:非负裁剪
Counter({'a': 1})
>>> Counter("aab") & Counter("abb") # 交集:取较小计数
Counter({'a': 1, 'b': 2})
>>> Counter("aab") | Counter("abb") # 并集:取较大计数
Counter({'a': 2, 'b': 2})底层加速:Counter.update/Counter.__iadd__ 在 CPython 里走 _count_elements(Modules/_collectionsmodule.c)——一个专门优化的 C 循环,比"for 循环手动数"快一个量级:
# timeit 对比:手动计数 vs Counter(10^6 个元素的列表)
>>> from collections import Counter
>>> import timeit
>>> data = list(range(1000)) * 1000
>>> timeit.timeit(lambda: Counter(data), number=10)
0.042
>>> def manual():
... d = {}
... for x in data:
... d[x] = d.get(x, 0) + 1
... return d
>>> timeit.timeit(manual, number=10)
0.61 # Counter 快约 15 倍(C 循环 vs Python 循环)🔑 性能洞察:"能用
Counter的计数"与"手写for计数"的差距来自 C 层_count_elements对 dict 操作的批量优化。规则:标准库里已有数据结构的场景,先查collections再手写——你手写的 Python 循环几乎不可能比 C 加速版本快。
Python 3.6 之前,dict 的迭代顺序是"未定义的"(实际是哈希表槽位顺序,随插入/删除剧烈变化)。OrderedDict(PEP 372,2.7/3.1)用双向链表记录插入顺序。
PEP 468(3.6)让 dict 保持插入顺序(CPython 实现细节,3.7 起成为语言规范,PEP 567 确认)。从此 OrderedDict 的"保序"价值消失,但它的专门 API 依然有用:
>>> from collections import OrderedDict
>>> od = OrderedDict([("a", 1), ("b", 2), ("c", 3)])
>>> od.move_to_end("a") # 把 a 移到末尾(LRU 缓存的核心操作!)
>>> od
OrderedDict([('b', 2), ('c', 3), ('a', 1)])
>>> od.popitem(last=False) # 从头部弹出(FIFO 队列语义)
('b', 2)版本注意:3.7+ 后,新代码用普通
dict(保序是语言规范);只有需要move_to_end/popitem(last=...)这类位置操作时才用OrderedDict。functools.lru_cache内部至今仍用OrderedDict——因为它需要"最近使用项移到末尾",这正是move_to_end的看家本领(见 13.4.2)。
deque(double-ended queue)是双向链表(block 数组链表)的 C 实现:两端增删都是 O(1),但中间索引是 O(n)。
>>> from collections import deque
>>> dq = deque([1, 2, 3])
>>> dq.append(4) # 右端 O(1)
>>> dq.appendleft(0) # 左端 O(1)
>>> dq
deque([0, 1, 2, 3, 4])
>>> dq.popleft() # 左端弹出 O(1)
0底层结构(Modules/_collectionsmodule.c 的 dequeobject):由 block 组成的链表,每个 block 是一个固定大小的数组(默认 64 元素),leftblock/rightblock 指针 + 左右索引维护两端。满则新开 block,空则回收——这就是"两端 O(1)"的来源(对比 list 的头部插入要整体搬移 O(n))。
# 经典场景:有界滑动窗口
>>> from collections import deque
>>> history = deque(maxlen=3) # 满 3 个自动丢最旧的
>>> for i in range(10):
... history.append(i)
>>> history
deque([7, 8, 9], maxlen=3) # 永远只保留最近 3 个| 操作 | list |
deque |
|---|---|---|
| 尾部 append/pop | O(1) | O(1) |
| 头部 insert/pop | O(n)(整体搬移) | O(1) |
中间索引 d[i] |
O(1) | O(n)(链表遍历) |
| 内存 | 连续数组(紧凑) | block 链表(有开销) |
⚠️ 陷阱:deque的随机访问是 O(n)——dq[500_000]要从头遍历。如果你"主要随机访问 + 偶尔两端增删",list反而更好。deque的正确用途是队列/栈/滑动窗口(两端口操作密集)。threading的queue.Queue内部就是deque+ 锁(第 11 章展开)。
>>> from collections import ChainMap
>>> defaults = {"theme": "dark", "lang": "en"}
>>> user = {"lang": "zh"}
>>> merged = ChainMap(user, defaults) # 前面的优先
>>> merged["lang"] # 命中 user
'zh'
>>> merged["theme"] # user 没有 → 落到 defaults
'dark'查找语义:从前到后扫描各映射,第一个命中即返回。写入语义:只写第一个映射(merged["x"] = 1 只改 user,不改 defaults)。
>>> merged["new_key"] = "v" # 写进最前面的映射
>>> "new_key" in defaults
False经典场景:配置覆盖链(命令行 > 用户配置 > 默认配置)。与"浅合并字典"({**user, **defaults})的区别:ChainMap 是视图,底层字典改动立即可见;浅合并是快照,改底层不会反映。
实战建议:需要"多层配置、可叠加、可动态增删层"用
ChainMap;只需要"一次性合并快照"用dict解包。ChainMap的new_child()还能优雅地实现"上下文压栈"(类似第 8 章contextlib的栈式管理)。
>>> from collections import namedtuple
>>> Point = namedtuple("Point", ["x", "y"])
>>> p = Point(1, 2)
>>> p.x, p.y # 属性访问
(1, 2)
>>> p[0], p[1] # 仍是元组,可索引
(1, 2)
>>> p._replace(x=10) # 返回新实例(不可变)
Point(x=10, y=2)
>>> p._asdict()
{'x': 1, 'y': 2}机制:namedtuple 用元类动态生成一个新类(第 12 章元编程的温和示例)——生成的类继承 tuple,把字段名编译进 __slots__ 或属性描述符,同时生成 _replace/_asdict/_make 等工具方法。
⚠️ 陷阱:
- 字段名不能是关键字/以下划线开头(
_replace冲突),非法名在创建时抛ValueError;namedtuple的默认值用defaults参数(3.7+)从右侧开始填充;- 需要可变的带名字结构?用
dataclass(第 7 章)——namedtuple是"不可变 + 轻量"场景的正解,dataclass是"可变 + 完整类型"场景的正解。
>>> from collections import UserDict
>>> class MyDict(UserDict): # 把 dict 当"成员"而非"父类"
... def __setitem__(self, key, value):
... super().__setitem__(key, str(value).upper())
>>> d = MyDict(a=1)
>>> d["a"]
'1'UserDict/UserList/UserString 是"以组合方式包装内置容器"的基类。为什么不用直接继承 dict?
# 直接继承 dict 的经典翻车:__setitem__ 与 update 不一致
class UpperDict(dict):
def __setitem__(self, k, v):
super().__setitem__(k, str(v).upper())
d = UpperDict()
d["a"] = 1 # 走 __setitem__ → '1'
d.update(b=2) # ⚠️ update 是 C 实现,绕过 __setitem__ → 2(小写!)
d["b"] # 2,不是 '2'dict 的 update/setdefault 等方法是 C 层直接操作,不会回调 Python 的 __setitem__;UserDict 则把 update 等所有方法都重新实现为调用 self.data 的 __setitem__,保证子类重写处处生效。
🔑 机制洞察:继承 C 实现的容器(
dict/list/str)时,重写的方法可能被内部 C 路径绕过(update、__init__、__getitem__链)。UserDict的存在就是为了让你"安全地子类化"。规则:只要你的子类要重写行为(不只是加方法),就用UserDict/UserList/UserString;只是加方法则直接继承内置容器即可。
大纲 13.1.1 的"数据与容器"象限里还有三个算法型模块——它们解决的是"容器 + 特定算法"的组合问题。
>>> import bisect
>>> data = [1, 3, 5, 7, 9]
>>> bisect.bisect_left(data, 6) # 插入点(保持有序):6 应该插在索引 3(7 之前)
3
>>> bisect.bisect_right(data, 5) # 5 的右侧插入点:索引 3
3
>>> bisect.insort(data, 6) # 插入并保持有序
>>> data
[1, 3, 5, 6, 7, 9]bisect 在有序序列上做二分查找——O(log n) 对比线性扫描的 O(n):
>>> import timeit, random, bisect
>>> data = sorted(random.sample(range(10**7), 10**6))
>>> timeit.timeit(lambda: 5_000_000 in data, number=100) # 线性 in
0.35
>>> timeit.timeit(lambda: bisect.bisect_left(data, 5_000_000), number=100)
0.006 # 二分快约 60 倍
⚠️ 陷阱:bisect要求列表已有序(升序)——无序列表上结果无意义。bisect_left与bisect_right的区别在重复元素:前者返回第一个 ≥ x 的位置,后者返回第一个 > x 的位置(left用于"找第一个",right用于"找插入到相同元素之后")。判断存在性用i = bisect_left(...); i < len(data) and data[i] == x。
heapq 把列表原地变成堆(min-heap),实现优先队列:
>>> import heapq
>>> heap = []
>>> heapq.heappush(heap, 5)
>>> heapq.heappush(heap, 1)
>>> heapq.heappush(heap, 3)
>>> heap # 堆序:heap[0] 永远最小
[1, 5, 3]
>>> heapq.heappop(heap) # 弹出最小元素(O(log n))
1
>>> heapq.nlargest(2, [3, 1, 4, 1, 5]) # 前 N 大(不排序整个列表)
[5, 4]
>>> heapq.nsmallest(3, [3, 1, 4, 1, 5])
[1, 1, 3]机制:heapq 操作普通 list,内部维持堆性质(父 ≤ 子)——heappush/heappop 都是 O(log n),heap[0] 恒为最小元素。heapify(list) 可在 O(n) 内把任意列表堆化。
# 经典场景:Top-K 问题(100 万个数里最大的 100 个)
>>> data = random.sample(range(10**8), 10**6)
>>> heapq.nlargest(100, data) # O(n log k),远优于全排序 O(n log n)
# 任务调度:按优先级取任务
>>> tasks = []
>>> heapq.heappush(tasks, (3, "low")) # (优先级, 任务)
>>> heapq.heappush(tasks, (1, "urgent"))
>>> heapq.heappush(tasks, (2, "normal"))
>>> heapq.heappop(tasks)
(1, 'urgent') # 永远先取优先级最小的🔑 机制洞察:
heapq是"用数组实现树"的经典数据结构——堆节点i的父节点是(i-1)//2,子节点是2i+1/2i+2,完全不需要指针。lru_cache的淘汰、Counter.most_common(13.2.1 源码里见过heapq.nlargest)、调度器全都依赖它。
>>> from array import array
>>> arr = array("i", [1, 2, 3]) # 'i' = signed int(类型码)
>>> arr.append(4)
>>> arr[0] = 10
>>> arr.tobytes() # 直接导出内存字节
b'\n\x00\x00\x00\x02\x00\x00\x00...'array 是固定类型的紧凑数组:array('i', [1,2,3]) 每个元素 4 字节,而 list 的每个元素是指向 Python 对象的指针(8 字节)+ 对象本身的开销。
| 维度 | list |
array('d') |
numpy.ndarray(卷 2) |
|---|---|---|---|
| 元素类型 | 任意 | 单一(类型码) | 单一(dtype) |
| 内存 | 指针数组 + 对象 | 连续原始字节 | 连续原始字节 |
| 数值运算 | 无(要循环) | 无 | 向量化 |
| 适用 | 通用 | 大文件二进制缓冲(第 9 章 struct 配合) |
数值计算 |
实战建议:
array的定位是"二进制数据缓冲"——与struct/memoryview/mmap(第 9 章)配合做二进制协议、读大二进制文件。要数值计算(数学运算、广播)直接用卷 2 的numpy——array没有向量化,纯 Python 循环仍是瓶颈。
第 5 章建立了迭代器协议(__iter__/__next__)与生成器心智,itertools 是把"迭代"这件事做到极致的模块:全部惰性、全部组合、大部分有 C 加速。它适合"把循环写成数据流"的声明式风格。
>>> from itertools import count, cycle, repeat, takewhile
>>> list(takewhile(lambda n: n < 5, count(1))) # 无限计数 + 限界
[1, 2, 3, 4]
>>> list(takewhile(lambda c: c != "c", cycle("abc"))) # 无限循环
['a', 'b']
>>> list(repeat("x", 3)) # 重复 n 次
['x', 'x', 'x']count 的 start/step 支持浮点(count(0, 0.5)),但注意浮点累计误差(count(0.1, 0.1) 到 1.0 可能不精确)。cycle 无限复用可迭代对象(内部会缓存整个序列!)。
⚠️ 陷阱:无限迭代器必须配takewhile/islice/break使用,否则程序永不结束。这是itertools与列表最大的心智差异——列表是有限的,迭代器可能无限。
这三个函数把组合数学(第 5 章练习里提过概念)直接变成一行:
>>> from itertools import product, permutations, combinations, combinations_with_replacement
>>> list(product("AB", [1, 2])) # 笛卡尔积:|A|×|B| 个
[('A', 1), ('A', 2), ('B', 1), ('B', 2)]
>>> list(permutations("ABC", 2)) # 排列:P(3,2)=6 个(有序、不重复取)
[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
>>> list(combinations("ABC", 2)) # 组合:C(3,2)=3 个(无序)
[('A', 'B'), ('A', 'C'), ('B', 'C')]
>>> list(combinations_with_replacement("AB", 2)) # 有放回组合:C(2+2-1,2)=3 个
[('A', 'A'), ('A', 'B'), ('B', 'B')]数学定义速查:
| 函数 | 计数公式 | 有序? | 可重复取? |
|---|---|---|---|
product(a, b) |
|a| × |b| | ✅ | ✅(跨集合) |
permutations(s, r) |
n!/(n−r)! | ✅ | ❌ |
combinations(s, r) |
n!/(r!(n−r)!) | ❌ | ❌ |
combinations_with_replacement(s, r) |
(n+r−1)!/(r!(n−1)!) | ❌ | ✅ |
# 实战:三层嵌套循环 → 一行笛卡尔积
# ❌ 三层 for(缩进地狱 + 中间列表)
result = []
for a in xs:
for b in ys:
for c in zs:
result.append(process(a, b, c))
# ✅ product:一个迭代器搞定
from itertools import product
result = [process(a, b, c) for a, b, c in product(xs, ys, zs)]🔑 性能洞察:
product等函数返回惰性迭代器——product(xs, ys, zs)本身几乎零开销,元素按需生成。对比"先生成所有组合存列表"(内存 O(n³)),product的内存是 O(1)。组合爆炸时这是唯一的活路。
>>> from itertools import groupby
>>> data = [("a", 1), ("a", 2), ("b", 3), ("a", 4)]
>>> for key, group in groupby(data, key=lambda t: t[0]):
... print(key, list(group))
a [('a', 1), ('a', 2)] # 只对【相邻】的 a 分组!
b [('b', 3)]
a [('a', 4)] # 第三个 a 因为不与前一个 a 相邻,成了新组
⚠️ 陷阱:groupby只对相邻的同键元素分组——它基于"迭代顺序中键连续变化"的假设。要按键全局分组,必须先排序:groupby(sorted(data, key=key), key=key)。这个"必须先排序"是最常见的groupby误用。
>>> from itertools import islice, takewhile, dropwhile
>>> list(islice(range(100), 10, 20)) # 等价 range(100)[10:20],但惰性
[10, 11, ..., 19]
>>> list(takewhile(lambda x: x < 3, [1, 2, 3, 1])) # 取"直到不满足"为止
[1, 2]
>>> list(dropwhile(lambda x: x < 3, [1, 2, 3, 1])) # 丢弃"直到满足"为止
[3, 1]islice(it, start, stop) 与切片 it[start:stop] 的关键差异:列表切片是立即物化的新列表(O(n) 内存),islice 是惰性消费(O(1) 内存)——对大文件/大流只能 islice。
>>> from itertools import chain, tee, zip_longest, accumulate
>>> list(chain([1, 2], [3], "ab")) # 拼接多个可迭代对象
[1, 2, 3, 'a', 'b']
>>> list(accumulate([1, 2, 3, 4])) # 前缀和(默认加法)
[1, 3, 6, 10]
>>> list(accumulate([1, 2, 3], max)) # 前缀最大值(任意二元函数)
[1, 2, 3]
>>> list(zip_longest("ab", [1, 2, 3], fillvalue="-")) # 不等长 zip
[('a', 1), ('b', 2), ('-', 3)]tee(it, n) 把一个迭代器复制成 n 个独立迭代器——机制是"每个 tee 副本共享一个内部缓存队列":
>>> it1, it2 = tee(range(5))
>>> list(it1), list(it2) # 两个都能完整迭代
([0, 1, 2, 3, 4], [0, 1, 2, 3, 4])
⚠️ 陷阱:tee的代价是缓存——如果一个副本被完全消费而另一个还没开始,缓存要装下整个序列。tee适用于"两个消费者进度接近"的场景;若两个消费者进度差异巨大,内存会爆炸,不如直接两次生成。
# 对比:链式迭代 vs 中间列表(10^6 元素)
>>> import timeit
>>> data = range(1_000_000)
>>> def lazy():
... from itertools import islice, chain
... return sum(islice(chain(data, data), 100_000))
>>> def eager():
... return sum(list(chain(list(data), list(data)))[:100_000])
>>> timeit.timeit(lazy, number=10)
0.11
>>> timeit.timeit(eager, number=10)
1.83 # 惰性快 ~16 倍 + 内存几乎为零| 维度 | 惰性(itertools/生成器) | 物化(列表) |
|---|---|---|
| 内存 | O(1)(流式) | O(n)(全量) |
| 速度 | 快(省去中间结构) | 慢(反复分配/拷贝) |
| 可读性 | 链式声明式 | 命令式循环 |
| 调试 | 难(看不到中间值) | 易(list(...) 即得) |
实战建议:管道式数据处理(过滤 → 映射 → 取前 N)优先
itertools+ 生成器表达式;数据量小且要反复使用,直接列表更简单。调试惰性链时,用list(itertools.islice(chain, 5))取前几个元素"快照"查看——不要list()整个无限迭代器。
第 6 章讲了函数是一等公民、闭包、装饰器,functools 把这些能力的标准用法沉淀成模块——"高阶函数的工具箱"。
functools.partial(func, *args, **kwargs) 返回一个"参数预填的函数":
>>> from functools import partial
>>> def power(base, exp): return base ** exp
>>> square = partial(power, exp=2)
>>> square(5)
25
>>> square(5, exp=3) # 调用时还能覆盖预填参数
125机制:partial 对象内部存 func/args/keywords 三元组,__call__ 时合并参数再调用——本质是闭包的显式封装(第 6 章):
# partial 等价的手写闭包
def square(base): return power(base, exp=2)| 方式 | 特点 |
|---|---|
lambda x: power(x, 2) |
每次创建新函数对象,repr 难读 |
functools.partial(power, exp=2) |
可 picklable(部分情况)、repr 清晰、属性可查 |
>>> square.func, square.keywords
(<function power at 0x...>, {'exp': 2})经典用途:回调参数预填(GUI/事件回调传参)、适配 API(把"多参函数"变成"少参函数"喂给排序/映射等高阶函数)。partialmethod 是给类方法用的变体(把 self 之外的参数预填进方法)。
实战建议:能用
partial就别用lambda——partial的对象有func/args/keywords属性可调试、可序列化(pickle支持部分情况),lambda则是个黑盒。唯一例外:需要在调用时求值默认参数的场景(lambda x: power(x, n)里n是调用时闭包变量)才用lambda。
>>> from functools import lru_cache
>>> @lru_cache(maxsize=128)
... def fib(n):
... if n < 2: return n
... return fib(n-1) + fib(n-2)
>>> fib(100) # 没有缓存会指数爆炸,这里秒回
354224848179261915075
>>> fib.cache_info()
CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)机制(_lru_cache_wrapper,Modules/_functoolsmodule.c):有序字典(OrderedDict)+ 双向链表实现 LRU——命中则把键移到末尾(move_to_end,13.2.2 提到过!),容量满则淘汰头部的"最久未用"项。maxsize=None 时退化为无限缓存(functools.cache,3.9+,无 LRU 淘汰)。
# 性能实测:递归斐波那契 n=35
# 无缓存:约 1.5 秒(指数级 2^35 次调用)
# lru_cache:微秒级(35 次调用)适用条件:函数纯(相同参数 → 相同结果)、无副作用、参数可哈希。
⚠️ 陷阱:
- 可变参数不可哈希:
@lru_cache函数不能接收list/dict参数(需转tuple/frozenset);- 缓存污染:如果"同样的参数"在不同时间应返回不同结果(如读文件、查时间、依赖全局状态),缓存会返回旧值——这是最隐蔽的 bug;
typed=True(3.2+)区分1与1.0(默认视为相同键);- 内存:
maxsize不设或太大,缓存会永久驻留(见第 10 章sys.modules的"永不淘汰"类比);lru_cache装饰的函数不能被部分清缓存——用fib.cache_clear()全清,或升级到 3.9 的cache无淘汰版。
singledispatch 实现泛型函数(generic function)——根据第一个参数的类型分派到不同实现:
>>> from functools import singledispatch
>>> @singledispatch
... def to_str(x): # 默认实现
... return f"unknown: {type(x)}"
>>> @to_str.register(int)
... def _(x):
... return f"int: {x}"
>>> @to_str.register(list)
... def _(x):
... return f"list of {len(x)}"
>>> to_str(42)
'int: 42'
>>> to_str([1, 2])
'list of 2'
>>> to_str(3.14)
'unknown: <class 'float'>' # 未注册类型 → 默认实现机制:singledispatch 维护一个类型 → 实现的注册表(registry 字典),调用时按 type(arg) 查表,未命中则沿 MRO 找"最近的已注册父类型":
>>> to_str.registry
{<class 'object'>: <function to_str at ...>, <class 'int'>: <function _ at ...>, <class 'list'>: <function _ at ...>}与第 7 章多态的对比:继承多态是"调用者不知道类型,由对象自己决定行为";singledispatch 是"调用者知道类型,由注册表决定行为"——后者不需要修改原类就能扩展行为,特别适合"给第三方库的类型添加自己的处理"。
实战建议:
singledispatch是"开放-封闭原则"的标准实现(新增类型 → 新增register,不改动已有代码)。singledispatchmethod(3.8+)是类方法版本。注意:它只按第一个参数分派(所以叫 single);多参数分派需要第三方库(如multipledispatch)。
>>> from functools import reduce
>>> reduce(lambda a, b: a + b, [1, 2, 3, 4]) # 累计归约 = 10
10
>>> reduce(max, [3, 1, 4, 1, 5]) # 归约成最大值
5reduce 把"二元运算折叠整个序列"写成一行——但 Python 社区慎用它(Guido 本人曾想从内置函数移除 reduce,理由是"显式 for 循环更可读")。可用 sum/max/min 的场景优先内置函数。
>>> from functools import cmp_to_key
>>> # Python 3 移除 cmp 参数后,老式比较函数要用 cmp_to_key 适配
>>> sorted(["banana", "apple", "Cherry"], key=cmp_to_key(
... lambda a, b: (a.lower() > b.lower()) - (a.lower() < b.lower())))
['apple', 'banana', 'Cherry']>>> from functools import wraps
>>> def logged(func):
... @wraps(func) # 复制 __name__/__doc__/__module__ 等元信息
... def wrapper(*args, **kwargs):
... print(f"calling {func.__name__}")
... return func(*args, **kwargs)
... return wrapper
>>> @logged
... def hello(): "says hello"; return "hi"
>>> hello.__name__ # 没有 wraps 会是 'wrapper'
'hello'🔑 机制洞察:
wraps本质是update_wrapper(wrapper, func)——把func的__name__/__doc__/__module__/__dict__拷到wrapper上。没有它,装饰后的函数会"丢失身份"(调试、help()、pickle、文档生成都会出错)。写装饰器必加@wraps——这是第 6 章装饰器的规范补全,第 12 章元编程会从描述符层面再深挖。
时间处理是最容易出错的标准库领域之一——错误通常不是立即崩溃,而是"夏令时差一小时""跨时区差一天"这类静默错误。本节把机制讲透,让错误无从发生。
datetime 模块的核心是三个不可变类(全部 C 实现,Modules/_datetimemodule.c):
>>> from datetime import date, time, datetime
>>> date(2026, 8, 16) # 日期:年/月/日
datetime.date(2026, 8, 16)
>>> time(14, 30, 45) # 时间:时/分/秒/微秒
datetime.time(14, 30, 45)
>>> datetime(2026, 8, 16, 14, 30) # 日期 + 时间(最常用)
datetime.datetime(2026, 8, 16, 14, 30)C 层结构(PyDateTime_DateTime)把字段压缩打包:date 用 fold 位 + 天数;time 用位域存时分秒微秒;datetime 组合两者并附 tzinfo 指针。不可变 + 可哈希——所以 datetime 能当 dict 键、能进 set、能被 lru_cache 缓存。
>>> d = date(2026, 8, 16)
>>> d.weekday() # 周一=0 ... 周日=6
6
>>> d.isoformat() # ISO 8601
'2026-08-16'
>>> d.toordinal() # 儒略日序号(与 date.fromordinal 互逆)
740012设计哲学:
datetime类的不可变性不是偶然——时间值被到处传递、比较、哈希,可变的时间对象会导致灾难性的共享状态 bug(类似第 7 章字符串的不可变设计)。"改时间"只能产生新对象:d.replace(day=1)。
timedelta 表示时间差,内部只存三个字段:days/seconds/microseconds,其余单位(周、时、分)在构造时归一化进去:
>>> from datetime import timedelta
>>> td = timedelta(weeks=1, days=2, hours=3)
>>> td.days, td.seconds # 7+2 天;3 小时 = 10800 秒
(9, 10800)
>>> td.total_seconds() # 总秒数(浮点)
790200.0
>>> datetime(2026, 8, 16) + timedelta(days=10) # 日期运算
datetime.datetime(2026, 8, 16, 0, 0) + 10 天 → 2026-08-26timedelta 只精确到微秒(microseconds 字段)——total_seconds() 返回浮点会引入舍入误差(第 3 章浮点陷阱的又一现场)。纳秒级需求(如性能计时)用 time.perf_counter_ns() 返回整数纳秒。
⚠️ 陷阱:timedelta的归一化是有符号的——timedelta(days=-1, seconds=1)是-1 天 + 1 秒 = -86399 秒,days=-1但seconds=86399。涉及负数时days/seconds的绝对值关系反直觉,用total_seconds()判断正负更稳。
>>> from datetime import datetime, timezone, timedelta
>>> naive = datetime(2026, 8, 16, 14, 30) # 无时区信息
>>> naive.tzinfo is None
True
>>> aware = datetime(2026, 8, 16, 14, 30, tzinfo=timezone.utc) # 带时区
>>> aware.tzinfo
datetime.timezone.utcnaive(裸时间)与 aware(带时区)不能混比——比较/相减会抛 TypeError:
>>> naive - aware
Traceback (most recent call last):
...
TypeError: can't subtract offset-naive and offset-aware datetimes>>> from zoneinfo import ZoneInfo
>>> sh = datetime(2026, 8, 16, 14, 30, tzinfo=ZoneInfo("Asia/Shanghai"))
>>> sh.utcoffset()
datetime.timedelta(seconds=28800) # UTC+8
>>> sh.astimezone(timezone.utc) # 转换到 UTC
datetime.datetime(2026, 8, 16, 6, 30, tzinfo=datetime.timezone.utc)zoneinfo 直接读取系统/打包的 IANA 时区数据库(tzdata 包提供),自动处理 DST(夏令时)——这是 pytz 时代最大的痛点:
# ❌ pytz 反模式(老代码常见):pytz 的 tzinfo 对象不能直接当 tzinfo 用
import pytz
dt = datetime(2026, 1, 1, tzinfo=pytz.timezone("Asia/Shanghai")) # 错误!
dt = pytz.timezone("Asia/Shanghai").localize(datetime(2026, 1, 1)) # 必须 localize
# ✅ zoneinfo:tzinfo 就是正经 tzinfo,直接用
from zoneinfo import ZoneInfo
dt = datetime(2026, 1, 1, tzinfo=ZoneInfo("Asia/Shanghai"))版本注意:
zoneinfo是 Python 3.9+(PEP 615)。系统没有 IANA 数据库时(Windows 部分环境),装tzdata包即可。新代码一律zoneinfo,不要再引入pytz。
夏令时切换日有"两个 01:30"(秋季回拨,fold=0/1)或"没有 02:30"(春季前拨):
>>> from zoneinfo import ZoneInfo
>>> ny = ZoneInfo("America/New_York")
>>> dt = datetime(2026, 11, 1, 1, 30, tzinfo=ny) # 秋季回拨日
>>> dt.fold # fold=0:第一次 1:30
0
>>> dt.replace(fold=1) # fold=1:第二次 1:30(DST 后)
datetime.datetime(2026, 11, 1, 1, 30, fold=1, tzinfo=ZoneInfo(key='America/New_York'))
⚠️ 陷阱:不要自己实现 DST 逻辑("UTC+8 就加 8 小时"在 UTC 无 DST 的时区成立,但在纽约/欧洲会错半年)。zoneinfo已封装全部规则;你唯一要遵守的铁律是:内部一律用 UTC(aware),展示时才转本地时区。
>>> from datetime import datetime
>>> # 格式化:strftime(datetime → str)
>>> datetime(2026, 8, 16, 14, 30).strftime("%Y-%m-%d %H:%M:%S")
'2026-08-16 14:30:00'
>>> # 解析:strptime(str → datetime)
>>> datetime.strptime("2026-08-16 14:30:00", "%Y-%m-%d %H:%M:%S")
datetime.datetime(2026, 8, 16, 14, 30)| 格式码 | 含义 | 示例 |
|---|---|---|
%Y / %y |
四位 / 两位年 | 2026 / 26 |
%m / %d |
零填充月 / 日 | 08 / 16 |
%H / %M / %S |
时 / 分 / 秒 | 14 / 30 / 00 |
%z / %Z |
UTC 偏移 / 时区名 | +0800 / CST |
%A / %a |
星期全名 / 缩写 | Sunday / Sun |
%B / %b |
月全名 / 缩写 | August / Aug |
%f |
微秒 | 000000 |
# 性能对比:strptime vs fromisoformat(10^5 次)
>>> import timeit
>>> s = "2026-08-16T14:30:00"
>>> timeit.timeit(lambda: datetime.strptime(s, "%Y-%m-%dT%H:%M:%S"), number=100_000)
0.38
>>> timeit.timeit(lambda: datetime.fromisoformat(s), number=100_000)
0.11 # fromisoformat 快 ~3.5 倍(专用 C 解析路径)实战建议:能交换 ISO 8601 格式(
2026-08-16T14:30:00)就只用 ISO——fromisoformat快、且isoformat()是标准交换格式(JSON API、数据库、跨语言)。strptime留给非标准格式(日志、用户输入)。3.11+ 的fromisoformat支持更多格式(含时区后缀)。
date.today()用本地时区:date.today()等价datetime.now().date()——服务器时区不是 UTC 时,与 UTC 日期比对会差一天。规则:跨机器/跨时区一律datetime.now(timezone.utc)。- aware 一致性:同一逻辑里要么全 naive(且明确是 UTC),要么全 aware——混用必出
TypeError或静默错值。 timestamp()的往返:datetime(2026,8,16, tzinfo=timezone.utc).timestamp()返回 POSIX 秒;naive datetime 的timestamp()会假设本地时区——同样的datetime对象在不同时区的机器上timestamp()不同。- 性能:
datetime对象创建是 C 级,快;但strptime/strftime按格式解析较慢——高频路径缓存格式化结果。 timedelta精度:微秒级;需要纳秒计时用time.perf_counter_ns()(第 15 章性能基准的标准工具)。
calendar 模块是"日历计算"的现成工具箱(农历之外的公历):
>>> import calendar
>>> calendar.isleap(2024) # 闰年判断
True
>>> calendar.weekday(2026, 8, 16) # 星期几(周一=0)
6
>>> calendar.monthrange(2026, 2) # (该月第一天是周几, 该月天数)
(6, 28) # 2026-02-01 是周日,2 月 28 天
>>> calendar.month(2026, 8) # 文本日历
' August 2026\nMo Tu We Th Fr Sa Su\n...'实用场景:monthrange 做"当月的天数/第一天星期"计算(排班、报表按月分页);TextCalendar/HTMLCalendar 生成日历视图。注意:calendar.weekday 的星期约定是周一=0,与 date.weekday() 一致(周日常见的"周日=0"约定在 ISO 体系里不同,见 date.isoweekday() 周日=7)。
第 3 章讲过 int/float 的表示与浮点精度问题,本节是"数字的工程工具箱"——在浮点不够精确、随机不安全、统计要可靠时怎么选。
math 是 C 标准库 <math.h> 的 Python 包装,纯计算、无状态:
>>> import math
>>> math.sqrt(2), math.log(100, 10), math.exp(1)
(1.4142135623730951, 2.0, 2.718281828459045)
>>> math.floor(3.7), math.ceil(3.2), math.trunc(-3.7)
(3, 4, -3)
>>> math.gcd(12, 18), math.lcm(4, 6) # 3.9+ 有 lcm
(6, 12)
>>> math.comb(5, 2), math.perm(5, 2) # 组合数/排列数(13.3.2 的公式实现)
(10, 20)浮点比较的正确姿势(第 3 章陷阱的标准解药):
>>> 0.1 + 0.2 == 0.3
False
>>> math.isclose(0.1 + 0.2, 0.3) # 相对容差比较
True
>>> math.isclose(1e10 + 1, 1e10, rel_tol=1e-9) # 相对容差 1e-9 时"相等"
True
>>> math.isfinite(1e308 * 10) # 溢出检测(∞ 不是错误,是值)
Falsecmath 是复数版(cmath.sqrt(-1) → 1j)。math 的 C 包装意味着快(无 Python 层循环),但慢路径(如 math.sin 的高精度实现)仍受 C 库精度影响——math 保证"结果正确舍入到 1 ulp 内"级别的精度承诺。
实战建议:判断"两个浮点是否相等"永远用
math.isclose(指定rel_tol);判断"数字是否异常"用isfinite而非== float('inf')(nan与任何值都不相等,x == x对nan是 False)。
>>> 0.1 + 0.2
0.30000000000000004 # 二进制浮点无法精确表示 0.1
>>> from decimal import Decimal
>>> Decimal("0.1") + Decimal("0.2")
Decimal('0.3') # 十进制精确float 是二进制浮点(0.1 是无限循环二进制小数);Decimal 是十进制浮点——对"人类十进制记账"(货币、税率、百分比)精确。财务/金额计算必须用 Decimal,这是行业铁律。
>>> Decimal("0.1") + Decimal("0.2") == Decimal("0.3")
True
>>> # ⚠️ 从字符串构造!从 float 构造会继承二进制误差
>>> Decimal(0.1) # ❌ 这不是"精确的 0.1"
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal("0.1") # ✅ 精确
Decimal('0.1')>>> from decimal import getcontext
>>> getcontext().prec = 28 # 默认 28 位有效数字
>>> getcontext().rounding # 默认 ROUND_HALF_EVEN(银行家舍入)
'ROUND_HALF_EVEN'
>>> Decimal("1") / Decimal("3")
Decimal('0.3333333333333333333333333333') # 28 位
>>> getcontext().prec = 4
>>> Decimal("1") / Decimal("3")
Decimal('0.3333')getcontext() 是线程局部的(threading.local 实现)——每个线程有自己的精度上下文。localcontext() 上下文管理器可以临时改精度(第 8 章的 with 协议又一实例)。
⚠️ 陷阱:
- 性能:
Decimal是纯 Python 实现(_decimal的 C 版本 3.3+ 是加速版),比float慢 10–100 倍——只用于"必须精确"的值,不要给性能热路径用;- 上下文敏感:同样表达式在不同
prec下结果不同——不要全局改getcontext().prec,用localcontext()局部改;- 与 float 混算:
Decimal("0.1") + 0.2抛TypeError(不允许隐式混算),需显式Decimal("0.2");ROUND_HALF_EVEN(银行家舍入)与直觉的"四舍五入"(ROUND_HALF_UP)不同:Decimal("0.5").quantize(Decimal("1"))得0(偶数),0.5四舍五入直觉是1。财务系统要确认舍入模式。
>>> from fractions import Fraction
>>> Fraction(1, 3) + Fraction(1, 6)
Fraction(1, 2) # 精确的 1/2,不是 0.5 的浮点近似
>>> Fraction("0.25")
Fraction(1, 4)
>>> Fraction(0.1) # ⚠️ 从 float 构造得到的是"0.1 的精确二进制值"
Fraction(3602879701896397, 36028797018963968)Fraction 内部存 (numerator, denominator) 且自动约分。适用:需要精确有理运算的数学/科学场景(概率论、线性代数推导)。与 Decimal 的取舍:Fraction 是"精确的有理数",Decimal 是"精确的十进制数"——前者适合数学推导,后者适合记账。
实战建议:概率/组合计算(如
Fraction(1, 6) ** 2)用Fraction避免浮点误差累积;结果需要浮点时再float(fr)。Fraction也能当 dict 键(可哈希、不可变)。
>>> import random
>>> random.seed(42) # 固定种子 → 可复现
>>> random.random() # [0, 1) 均匀分布
0.6394267984578837
>>> random.randint(1, 6) # [1, 6] 整数
2
>>> random.choice(["a", "b", "c"])
'b'
>>> random.sample(range(100), 5) # 不重复抽样
[81, 14, 3, 94, 35]
>>> random.shuffle(["a", "b", "c"]) # 原地洗牌机制:random 默认用 Mersenne Twister(梅森旋转)——一个伪随机数生成器(PRNG):给定种子,序列完全确定。seed(42) 让实验可复现(科学计算、测试的黄金习惯)。
分布:random.gauss(mu, sigma)(正态)、random.expovariate(lambd)(指数)、random.betavariate 等——内部用 Box-Muller 等算法从均匀分布变换而来。
⚠️ 安全红线:random是伪随机,绝不能用于密码学(token、密码、密钥、验证码)——Mersenne Twister 的 624 个输出即可预测整个序列。安全随机用secrets:
>>> import secrets
>>> secrets.token_hex(16) # 加密安全随机 token
'3f2a8c...'
>>> secrets.choice(["a", "b"]) # 安全选择(如密码重置码)| 场景 | 模块 | 原因 |
|---|---|---|
| 测试/模拟/游戏 | random |
可复现(seed) |
| 抽样统计 | random.sample |
伪随机足够 |
| token/密码/密钥 | secrets |
加密安全(os.urandom 后端) |
| 数值实验 | numpy.random(卷 2) |
批量 + 可复现 |
>>> import statistics
>>> statistics.mean([1, 2, 3, 4, 5])
3
>>> statistics.median([1, 2, 3, 4, 100]) # 中位数抗离群值
3
>>> statistics.stdev([1, 2, 3, 4, 5]) # 样本标准差(n-1 分母)
1.5811388300841898
>>> statistics.pstdev([1, 2, 3, 4, 5]) # 总体标准差(n 分母)
1.4142135623730951
⚠️ 陷阱:
mean对超大数求和可能溢出/精度损失——3.8+ 的fmean用math.fsum做精确累加(Neumaier 算法),大数据集用fmean;stdev(样本)与pstdev(总体)分母不同(n−1 vs n)——别混用,样本推断用stdev;- 空输入抛
StatisticsError(不是ValueError)——异常类型要接对;- 数据量级差异大时,
statistics的简单实现可能精度不足——正式统计用卷 2 的numpy/scipy.stats。
第 9 章讲了"文件字节流"的底层 IO,本节是系统层的高阶工具:调外部程序、批量文件操作、路径通配、平台识别。
调外部命令是 Python 工程的高频需求(git、ffmpeg、系统工具),subprocess 是唯一正解(os.system 早已是反模式)。
>>> import subprocess
>>> # 高阶 API:run() 一步到位
>>> result = subprocess.run(
... ["git", "status", "--short"], # ✅ 参数列表,不用 shell 拼接
... capture_output=True, # 捕获 stdout/stderr
... text=True, # 解码为文本(3.7+)
... timeout=10, # 超时保护
... )
>>> result.returncode
0
>>> result.stdout
' M python-core/py-13-stdlib.md\n'# 失败即报错:check=True
>>> subprocess.run(["false"], check=True)
Traceback (most recent call last):
...
subprocess.CalledProcessError: Command '['false']' returned non-zero exit status 1.
# 需要交互/流式:Popen(底层 API)
>>> proc = subprocess.Popen(
... ["python", "-c", "import sys; print(sys.version)"],
... stdout=subprocess.PIPE, text=True,
... )
>>> out, err = proc.communicate(timeout=5) # 等待并读取# ❌ 危险:shell 拼接字符串 = 命令注入
import subprocess
user_input = "1; rm -rf /" # 恶意输入
subprocess.run(f"echo {user_input}", shell=True) # 执行了 rm -rf /!
# ✅ 安全:参数列表 + shell=False(默认)
subprocess.run(["echo", user_input]) # 当作参数,不解析shell=True 会把命令交给系统 shell 解析——任何未转义的用户输入都可能变成新命令(;、&&、|、反引号、$())。铁律:
- 默认
shell=False(run的默认),传参数列表; - 需要管道/重定向等 shell 特性时,用 Python 侧实现(
stdout=PIPE链式)而非 shell 语法; - 真的必须
shell=True(如调用 shell 内建),绝不拼接用户输入。
# ✅ 用 Python 实现"管道":cmd1 | cmd2
p1 = subprocess.Popen(["dir"], stdout=subprocess.PIPE, text=True)
p2 = subprocess.Popen(["findstr", "py"], stdin=p1.stdout, stdout=subprocess.PIPE, text=True)
p1.stdout.close() # 关键:关闭父进程的管道副本,避免死锁
out, _ = p2.communicate()
⚠️ 陷阱:
- 子进程继承环境变量——敏感信息(密钥)别放环境变量传给不可信子进程;
capture_output=True时 stdout/stderr 是内存缓冲——子进程输出巨大(>64KB 管道缓冲)可能死锁,此时用Popen+communicate或写文件;timeout超时后run抛TimeoutExpired,但子进程可能还在运行(kill由你负责,Popen场景用proc.kill())。
第 9 章的 open()/pathlib 解决"单个文件的读写",shutil 解决"文件集合的操作":复制、移动、删除目录树、归档。
>>> import shutil
>>> shutil.copy("a.txt", "b.txt") # 复制内容(不保留元数据)
'b.txt'
>>> shutil.copy2("a.txt", "c.txt") # 复制内容 + 元数据(时间戳等)
'c.txt'
>>> shutil.move("c.txt", "sub/") # 移动(跨文件系统也能用)
'sub/c.txt'
>>> shutil.rmtree("old_dir") # 递归删除目录(⚠️ 不可恢复)
>>> shutil.copytree("src", "dst") # 递归复制目录
>>> shutil.disk_usage("C:\\") # 磁盘用量
usage(total=..., used=..., free=...)
>>> shutil.make_archive("backup", "zip", "mydir") # 打包为 zip/tar
'backup.zip'| 函数 | 保留内容 | 保留元数据 | 备注 |
|---|---|---|---|
copy |
✅ | ❌ | 内容复制 |
copy2 |
✅ | ✅ | 尽可能保留元数据 |
copytree |
✅ | 可选(copy_function 参数) |
目录递归 |
move |
✅ | ✅ | 同文件系统是 rename,跨系统是复制+删 |
⚠️ 陷阱:
shutil.copyvscopy2:默认copyfile不保留时间戳——备份场景要用copy2;rmtree对只读文件在 Windows 上会失败(3.8+ 有onexc回调处理)——删不掉时检查文件属性;copytree默认目标不存在才复制,dirs_exist_ok=True(3.8+)允许合并到已有目录;make_archive的根目录行为:root_dir/base_dir参数控制归档内路径结构,细节见文档——"打包出来多一层目录"是经典困惑。
>>> import glob
>>> glob.glob("python-core/*.md") # 单层通配
['python-core/py-01-environment-setup.md', ...]
>>> glob.glob("**/*.md", recursive=True) # 递归(** 需要 recursive=True)
['README.md', 'python-core/py-01-...md', ...]
>>> glob.iglob("*.py") # 惰性版本(大目录省内存)glob 的通配语法(*/?/[...])与正则不是一回事——它是"文件路径通配",比正则简单,也别混用:
| 语法 | glob(路径通配) | 正则(re) |
|---|---|---|
| 任意字符 | * |
.* |
| 单字符 | ? |
. |
| 字符集 | [abc] |
[abc] |
| 任意数量(含 0) | * |
* |
fnmatch 提供单名字匹配(fnmatch.fnmatch("a.py", "*.py"));pathlib 的 Path.glob()(第 9 章讲过)是面向对象版本,优先用它。
实战建议:现代代码用
pathlib.Path.glob()(返回Path对象,天然跨平台);glob模块适合"只要字符串路径"或需要recursive深层扫描的场景。注意glob不排序(sorted()包一层)。
"检测操作系统"有三个 API,含义完全不同:
>>> import platform, sys, os
>>> platform.system() # 人类可读:'Windows' / 'Linux' / 'Darwin'
'Windows'
>>> sys.platform # 技术标识:'win32' / 'linux' / 'darwin'
'win32'
>>> os.name # 最粗粒度:'nt' / 'posix'
'nt'| API | 值示例 | 粒度 | 用途 |
|---|---|---|---|
platform.system() |
Windows/Linux |
展示/日志 | 报告给用户看 |
sys.platform |
win32/linux/darwin |
技术判断 | 代码分支的首选 |
os.name |
nt/posix |
家族级 | 判断 POSIX 语义(路径、信号) |
# ✅ 正确的跨平台分支
if sys.platform == "win32":
exe = "where"
elif sys.platform == "darwin": # macOS
exe = "which"
else: # linux 等
exe = "which"
# ❌ 反模式:用 platform.system() 做代码分支(值不稳定,且大小写混乱)
⚠️ 陷阱:sys.platform是技术标识(分支用它),platform.system()是展示名(日志/UI 用它)——别反着用。更精细的信息:platform.machine()(CPU 架构,wheel 标签里见过)、platform.python_version()、sys.maxsize(判断 32/64 位)。路径处理永远用pathlib(第 9 章),不要手写os.sep拼接。
第 9 章的 json/pickle 解决"对象 → 字节",本节解决"数据怎么存、参数怎么进"——数据库、表格文件、配置文件、命令行参数。
sqlite3 是 Python 内置的嵌入式 SQL 数据库(C 库,零配置、单文件)——不需要数据库服务器,一个 .db 文件即数据库。它是"中小规模结构化数据的默认答案"。
>>> import sqlite3
>>> conn = sqlite3.connect("app.db") # 不存在则创建
>>> cur = conn.execute("""
... CREATE TABLE IF NOT EXISTS users (
... id INTEGER PRIMARY KEY,
... name TEXT NOT NULL,
... age INTEGER
... )
... """)
>>> conn.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Alice", 30))
>>> conn.commit() # 事务提交(见下)
>>> rows = conn.execute("SELECT * FROM users WHERE age > ?", (25,)).fetchall()
>>> rows
[(1, 'Alice', 30)]# ❌ 字符串拼接 = SQL 注入
name = "'; DROP TABLE users; --"
conn.execute(f"SELECT * FROM users WHERE name = '{name}'") # 灾难
# ✅ 参数化:? 占位符,值由驱动转义
conn.execute("SELECT * FROM users WHERE name = ?", (name,))SQL 注入是 web 安全第一大漏洞(OWASP Top 10 常年榜首)——sqlite3 的 ? 占位符把值交给 C 库安全绑定,永远不要用 f-string/% 拼 SQL。
>>> conn = sqlite3.connect("app.db")
>>> conn.isolation_level # 默认 ''(开启隐式事务)
''
>>> # 默认行为:DML 语句自动开启事务,commit/rollback 结束
>>> conn.execute("INSERT ...")
>>> conn.commit() # 提交
# 或者
>>> conn.rollback() # 回滚# 推荐:with 块管理事务(3.12+ 真正支持)
with sqlite3.connect("app.db") as conn: # 3.12 前:with 只管理连接关闭,不自动提交!
conn.execute("INSERT INTO users ...") # 成功自动 commit,异常自动 rollback版本注意:3.12 之前
with sqlite3.connect(...)只负责关闭连接,不自动提交事务——旧代码里with块内必须手动commit(),否则数据丢失(这是 sqlite3 经典大坑)。3.12+ 的with才真正按事务语义工作。
⚠️ 陷阱:
- 线程:默认同一连接跨线程共享需
check_same_thread=False(有风险)——多线程用每线程一个连接;executemany批量插入比循环execute快一个量级(C 层批量绑定);row_factory = sqlite3.Row让行支持列名访问(row["name"])——比默认元组好用;- 并发写冲突抛
sqlite3.OperationalError: database is locked——写多读多的场景该考虑服务器数据库(PostgreSQL 等)。
>>> import csv
>>> with open("data.csv", "w", newline="", encoding="utf-8") as f: # ⚠️ newline='' 铁律
... writer = csv.writer(f)
... writer.writerow(["name", "age"])
... writer.writerow(["Alice", 30])
>>> with open("data.csv", newline="", encoding="utf-8") as f:
... for row in csv.reader(f):
... print(row)
['name', 'age']
['Alice', '30'] # ⚠️ 全是字符串,需自行转换类型
⚠️ 陷阱:
newline=''是铁律(官方文档明确要求):否则 Windows 上会写出\r\r\n双换行(CSV 规范本身用\r\n,newline=''让 Python 不做额外转换);- CSV 无类型:读出来全是
str——数值要自己int(row[1]);- 编码:必须显式
encoding=(第 9 章 PEP 686 之后默认 UTF-8,老文件可能是 GBK)——用utf-8-sig兼容带 BOM 的 Excel 导出文件;- 复杂 CSV(嵌套引号、换行字段)用
csv.DictReader/DictWriter或第三方pandas(卷 2);脏数据(多表头、非标准转义)直接上pandas。
# config.ini
[DEFAULT] # DEFAULT 段对所有段可见(继承语义)
debug = false
[database]
host = localhost
port = 5432
[logging]
level = INFO>>> import configparser
>>> cfg = configparser.ConfigParser()
>>> cfg.read("config.ini", encoding="utf-8")
>>> cfg["database"]["host"] # 段 + 键
'localhost'
>>> cfg.getint("database", "port") # 类型转换
5432
>>> cfg.getboolean("DEFAULT", "debug") # 布尔解析(true/false/yes/no/on/off/1/0)
False
>>> cfg["database"]["debug"] # DEFAULT 的值对子段可见
'false'机制要点:
DEFAULT段的继承:DEFAULT里的键对所有段可见(cfg["database"]["debug"]能读到);- 插值:默认开启
%(key)s引用——host = %(base_host)s会展开,配置里含字面%需写%%(3.5+ 可用ConfigParser(interpolation=None)关闭); - 大小写:默认保留大小写,但键查找大小写不敏感(
cfg["DATABASE"]也行); - 写回:
cfg.set(...)+with open("config.ini","w") as f: cfg.write(f)。
实战建议:INI 适合"人类手写、层级浅"的配置;复杂配置(嵌套、列表、环境变量替换)用
tomllib(3.11+,读取 TOML)+pyproject.toml或dotenv。不要用 JSON 当配置文件(无注释、难手写)——这是常见反模式。
tomllib(PEP 680,3.11+)是 TOML 配置的只读解析器——pyproject.toml(第 10 章)的标准读取方式:
>>> import tomllib
>>> with open("config.toml", "rb") as f: # ⚠️ 必须以二进制模式打开
... cfg = tomllib.load(f)
>>> cfg["database"]["host"]
'localhost'
>>> tomllib.loads('title = "demo"') # 从字符串解析
{'title': 'demo'}# config.toml —— 比 INI 表达力强:嵌套、数组、类型
[database]
host = "localhost"
port = 5432 # 真实类型(INI 全是字符串)
timeouts = [1, 2, 3] # 数组
[logging.level] # 嵌套表
app = "INFO"对比 configparser:TOML 自带类型(数字/布尔/数组/嵌套表),无需 getint/getboolean 转换;格式规范(pyproject.toml 同款)。注意 tomllib 只读——要写 TOML 用第三方 tomli_w/tomlkit。
版本注意:
tomllib是 3.11+(PEP 680);3.10 及更早用第三方tomli(API 一致,pip install tomli)。新项目的"人类可读配置"首选 TOML +tomllib,INI 留给遗留系统。
>>> import argparse
>>> parser = argparse.ArgumentParser(prog="mytool", description="Demo CLI")
>>> parser.add_argument("input", help="输入文件") # 位置参数(必填)
>>> parser.add_argument("-o", "--output", default="out.txt") # 可选参数
>>> parser.add_argument("-v", "--verbose", action="store_true") # 布尔开关
>>> parser.add_argument("--limit", type=int, default=10) # 类型转换
>>> args = parser.parse_args(["--limit", "5", "data.csv"]) # 测试时传 argv
>>> args.input, args.output, args.verbose, args.limit
('data.csv', 'out.txt', False, 5)| 参数形态 | 写法 | 语义 |
|---|---|---|
| 位置参数 | parser.add_argument("input") |
按顺序必填 |
| 可选参数 | -o / --output |
带值选项 |
| 布尔开关 | action="store_true" |
出现即 True |
| 类型转换 | type=int |
自动转换(非法值报错) |
| 多值 | nargs="+" |
收集为列表 |
| 选择 | choices=["a","b"] |
限定取值 |
| 子命令 | add_subparsers() |
git commit / git push 式 |
# 子命令:git 风格工具
parser = argparse.ArgumentParser(prog="tool")
sub = parser.add_subparsers(dest="command", required=True)
run_p = sub.add_parser("run")
run_p.add_argument("--config")
build_p = sub.add_parser("build")
build_p.add_argument("--clean", action="store_true")
args = parser.parse_args(["run", "--config", "dev.toml"])
>>> args.command, args.config
('run', 'dev.toml')实战模式:与第 10 章 10.8.3 的完整组合——
argparse解析 +[project.scripts]入口点 +__main__.py转发,就是现代 Python CLI 工具的标准骨架。argparse自动生成--help/--version(action="version")与错误信息,零样板。需要更现代/更花哨的 CLI(颜色、进度条、补全)再看第三方click/typer——但argparse零依赖,多数场景够用。
日志是生产系统的眼睛——print 在脚本里够用,在服务里是灾难(无级别、无时间戳、无法配置输出位置)。logging 是标准库的完整日志框架,理解它的四件套架构后,配置只是拼积木。
import logging
# 1. Logger:日志入口(你代码里用的)
logger = logging.getLogger("myapp") # 名字即层级(见 13.9.2)
logger.info("starting up")
# 2. Handler:日志去哪(控制台/文件/网络...)
handler = logging.StreamHandler() # 输出到 stderr
# 3. Formatter:日志长什么样
formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
handler.setFormatter(formatter)
# 4. Filter:要不要记(按条件过滤)
class SensitiveFilter(logging.Filter):
def filter(self, record):
return "password" not in record.getMessage()
# 组装
logger.addHandler(handler)
logger.addFilter(SensitiveFilter())
logger.setLevel(logging.INFO)记录流程(一条日志的旅程):
logger.debug/info/warning/error(...)
→ 检查级别:record.levelno >= logger 有效级别? 否 → 丢弃
→ 创建 LogRecord(时间、文件名、行号、消息...)
→ 传给本 logger 的 Handlers
→ 若 propagate=True(默认)→ 传给父 logger 的 Handlers(递归到 root)
→ 每个 Handler 检查自己的级别 → Formatter 格式化 → 输出
🔑 机制洞察:
LogRecord的%(filename)s/%(lineno)d来自栈回溯(sys._getframe)——日志语句调用点的信息,不是 logger 定义处。所以日志慢的根源之一是"每次都要爬栈";高频日志(每秒上万条)用logger.isEnabledFor(level)预检查,或直接让if logger.isEnabledFor(logging.DEBUG): logger.debug(...)短路。
>>> logging.getLogger("myapp") is logging.getLogger("myapp") # 同名同对象
True
>>> logging.getLogger("myapp.database")
<Logger myapp.database (WARNING)>getLogger 是单例注册表(与第 10 章 sys.modules、13.2 的注册表模式同构):名字用点分层级 myapp.database,与模块名天然对应——logging.getLogger(__name__) 是库的标准写法(第 10 章讲过模块名 mypkg.db → logger 名 mypkg.db,层级自动成立)。
>>> logger = logging.getLogger("myapp")
>>> logger.getEffectiveLevel() # 未显式设置时,向上找父级/root 的级别
20 # WARNING(root 默认 WARNING)传播(propagate):logger 处理完自己的 Handler 后,若 propagate=True,LogRecord 继续冒泡给父 logger 的 Handler——这就是"import 第三方库 后它打日志你也能看到"的原因(库 logger 默认 propagate 到 root)。
⚠️ 陷阱:
- 重复日志:父 logger 和子 logger 都挂了 Handler 时,一条日志会被记两次(子处理一次 + 冒泡到父再处理一次)。解决:子 logger 设
propagate = False,或只在 root 挂 Handler;- 库的规范:写库时只用
logging.getLogger(__name__),不要加 Handler(加 Handler 会污染使用方的输出)——库应加logging.NullHandler(防止 "No handlers" 警告),由使用方统一配置;getLogger("x")与getLogger("x.y")是父子关系(名字前缀),但getLogger不检查名字合法性——拼错名会静默创建新 logger(看不到日志的常见原因:getLogger("myapp")配置了,代码里写成getLogger("my_app"))。
# 方式 1:basicConfig(脚本/小工具够用)
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
filename="app.log", # 不写则输出到 stderr
)# 方式 2:dictConfig(应用级标准,可 JSON 化)
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {"format": "%(asctime)s %(levelname)s %(name)s: %(message)s"},
},
"handlers": {
"console": {"class": "logging.StreamHandler", "formatter": "default"},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "app.log",
"maxBytes": 1_000_000, # 1MB 轮转
"backupCount": 3,
"formatter": "default",
},
},
"root": {"level": "INFO", "handlers": ["console", "file"]},
}
logging.config.dictConfig(LOGGING_CONFIG)dictConfig 的优势:配置是数据结构——可以从 JSON/YAML 文件加载、可以由环境变量覆盖、可以测试。fileConfig 读 INI 是旧方案(不支持层级结构,新代码用 dictConfig)。
logger.info("user logged in", extra={"user_id": 42, "ip": "1.2.3.4"})
# Formatter 里引用 extra 字段(用 %(user_id)s)
# 生产推荐:JSON 结构化输出(可被日志平台解析)
class JsonFormatter(logging.Formatter):
def format(self, record):
import json
data = {
"ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
"level": record.levelname,
"logger": record.name,
"msg": record.getMessage(),
}
data.update(getattr(record, "extra_fields", {}))
return json.dumps(data, ensure_ascii=False)from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
# RotatingFileHandler:按大小轮转(maxBytes + backupCount)
# TimedRotatingFileHandler:按时间轮转(when="midnight" 每天一个文件)- 日志吞异常:
logger.exception("msg")在except块里自动附带堆栈(等价error(..., exc_info=True));logging.exception只能在 except 块内调用,否则exc_info无效; - 格式化惰性:
logger.debug(f"x={x}")的f-string无论级别是否启用都会求值——性能关键路径用logger.debug("x=%s", x)(%延迟格式化,级别不满足时零开销):# ❌ 总是格式化(即使 DEBUG 未启用) logger.debug(f"expensive={compute_expensive()}") # ✅ 惰性:compute_expensive() 只在启用时被调用 logger.debug("expensive=%s", compute_expensive())
printvslogging的取舍:脚本/教学代码用print(简单直接);任何"要排查问题"的程序用logging——时间戳、级别、调用点、输出重定向、轮转,全是print给不了的;- 不要全局改 root 级别:库和应用的日志级别在配置层管理(
dictConfig),代码里不写logging.basicConfig(level=...)(它会覆盖使用方的配置)。
实战模式:生产日志四要素——(1)
dictConfig集中配置;(2) JSON 结构化输出;(3) 轮转防无限增长;(4) 敏感字段脱敏(Filter,如密码/token)。这套组合让日志从"调试工具"变成"可检索的审计数据"。
QueueHandler + QueueListener:把日志写入内存队列,后台线程批量落盘——让"打日志"不阻塞主业务线程(日志 I/O 慢时的高性能方案):
import logging, logging.handlers, queue
q = queue.Queue(-1) # 无界队列
queue_handler = logging.handlers.QueueHandler(q)
file_handler = logging.FileHandler("app.log")
listener = logging.handlers.QueueListener(q, file_handler)
root = logging.getLogger()
root.addHandler(queue_handler)
root.setLevel(logging.INFO)
listener.start() # 后台线程消费队列写文件
# 业务代码 logging.info(...) 只做"入队"(微秒级),I/O 由监听线程完成请求关联 ID(correlation ID):一条请求的日志散落在多个模块里,用 contextvars(第 11 章的线程/协程本地变量)贯穿:
import contextvars
request_id = contextvars.ContextVar("request_id", default="-")
class RequestIdFilter(logging.Filter):
def filter(self, record):
record.request_id = request_id.get() # 注入每条日志记录
return True
# 请求入口:request_id.set(uuid4().hex)
# 之后任何模块的日志都自动带 request_id 字段(结构化日志可直接检索)🔑 机制洞察:
contextvars是"线程/协程局部变量"的标准机制(threading.local的升级版,异步场景可用)——它让"请求上下文"跨函数传递而不必显式传参。日志关联 ID 只是第一个应用,第 11 章并发与第 14 章测试都会用到它。QueueHandler+ 关联 ID + JSON 结构化 = 高吞吐服务的日志三件套。
类型标注(type hints)是 Python 3 最重要的"工程化"特性——它不改变运行时行为(默认),但让静态检查、IDE 智能、接口文档成为可能。
Python 是动态语言——类型错误在运行时才暴露。类型标注(PEP 484,3.5+)把"预期的类型"写进代码,由外部工具(mypy/pyright/pytype)静态检查:
def add(a: int, b: int) -> int: # 参数类型 + 返回类型
return a + b
add(1, "2") # 运行时:TypeError(字符串不能和 int 加)
# mypy 静态检查:error: Unsupported operand types for + ("int" and "str")$ mypy app.py # 静态检查,不运行代码
Success: no issues found in 1 source file| 价值 | 说明 |
|---|---|
| 静态检查 | mypy/pyright 在运行前抓住类型 bug(大型重构的安全网) |
| IDE 智能 | 补全、跳转、重构都基于类型信息 |
| 接口文档 | 签名自带"参数是什么、返回什么" |
| 运行时校验 | 结合 pydantic/dataclass(第 7 章)在边界处校验 |
设计哲学:Python 的取舍是"动态语言的运行时 + 静态语言的检查器"——类型标注默认不改变运行时行为(除非你主动用
pydantic/typeguard做运行时校验)。这让团队可以渐进采纳:先注释式标注,再上 mypy,最后在边界做运行时校验。
from typing import Optional, Union, Any, Literal
# Optional[X] == Union[X, None](X 或 None)
def find(name: str) -> Optional[int]: ... # 可能返回 None
# Union:多种可能
def parse(s: str) -> Union[int, float]: ...
# Any:任意(放弃检查——尽量少用)
def passthrough(x: Any) -> Any: ...
# Literal:字面值约束
def set_mode(mode: Literal["fast", "safe"]) -> None: ... # 只接受这两个字符串3.9+ 内置泛型(PEP 585)——typing.List 等旧写法让位给内置类型:
| 旧写法(3.8-) | 新写法(3.9+) |
|---|---|
List[int] |
list[int] |
Dict[str, int] |
dict[str, int] |
Tuple[int, str] |
tuple[int, str] |
Set[str] |
set[str] |
Optional[int] |
int | None(3.10+,PEP 604) |
Union[int, float] |
int | float(3.10+) |
def process(items: list[int]) -> dict[str, int]: ...
def maybe() -> int | None: ... # 3.10+ 的 Union 简写版本注意:
list[int]是 3.9+(PEP 585),int | None是 3.10+(PEP 604)。需要兼容老版本时用from __future__ import annotations(3.7+)让标注惰性求值(见 13.10.4),或继续用typing.Optional/Union。
函数级泛型:"对任意类型 T,函数行为一致":
from typing import TypeVar, Sequence
T = TypeVar("T") # 类型变量
def first(items: Sequence[T]) -> T: # 返回与元素同类型
return items[0]
first([1, 2, 3]) # T = int → int
first(["a"]) # T = str → strTypeVar 的约束与协变:
T = TypeVar("T", bound=Animal) # 约束:T 必须是 Animal 或其子类
def make(proto: type[T]) -> T: ... # type[T]:返回 T 的实例
# 协变/逆变(Covariant/Contravariant)——"List[Cat] 是不是 List[Animal]?"
# 规则:只读容器协变(List[Cat] 可当 List[Animal] 用——但可变容器不行!)
# ❌ def f(xs: list[Animal]) 不能传 list[Cat](list 可变,可往里塞 Dog)
# ✅ def f(xs: Sequence[Animal]) 可以(Sequence 只读,协变)🔑 机制洞察:可变容器不可协变是类型系统的核心安全规则——
list[Dog]不能当作list[Animal],否则你能append一只猫进"狗列表"。只读接口(Sequence/Mapping)协变安全。写泛型函数时:参数用只读类型(Sequence/Mapping)而非可变类型(list/dict),是"类型正确的泛型"的关键习惯。
Protocol(PEP 544,3.8+)——结构子类型:不要求继承,只要"有这些方法"就算满足(第 7 章讲过的鸭子类型的静态化):
from typing import Protocol
class SupportsClose(Protocol): # 结构协议:有 close() 就行
def close(self) -> None: ...
def cleanup(obj: SupportsClose) -> None: # 任何有 close() 的对象都接受
obj.close()
class File: # 不需要继承 SupportsClose!
def close(self) -> None: ...
class DB: # 也不需要
def close(self) -> None: ...
cleanup(File()); cleanup(DB()) # ✅ 类型检查通过(结构匹配)与第 7 章 abc.ABC 的对比:ABC 是运行时强制(isinstance 检查 + 抽象方法);Protocol 是静态检查的(isinstance 需 @runtime_checkable 装饰才可用,且只检查方法存在性)。
NewType——"名义上的新类型"(防止"用户名"和"ID"混用):
from typing import NewType
UserId = NewType("UserId", int) # 运行时就是 int
def get_user(uid: UserId) -> str: ...
get_user(123) # ❌ mypy 报错:int 不是 UserId
get_user(UserId(123)) # ✅ 显式包装@overload——同一函数"不同参数类型 → 不同返回类型"的声明(运行时只有最后一个实现):
from typing import overload
@overload
def parse(s: str) -> dict: ... # str → dict
@overload
def parse(b: bytes) -> list: ... # bytes → list
def parse(x): # 实际实现(无类型标注或 Any)
return json.loads(x) if isinstance(x, str) else list(x)
# 调用方视角:parse("{}") 的类型被推断为 dict,parse(b"") 为 list实战建议:团队引入
typing的进阶路线——先用基础标注(13.10.2)跑通 mypy → 加TypeVar泛型(13.10.3)→ 需要"鸭子类型静态化"时用Protocol→ 需要"防混用"时用NewType。overload是库作者优化 API 类型体验的工具,应用代码很少用。
>>> from typing import get_type_hints
>>> def f(x: int) -> str: return str(x)
>>> f.__annotations__ # 标注存在函数属性里
{'x': <class 'int'>, 'return': <class 'str'>}
>>> get_type_hints(f) # 解析字符串形式(如 "list[int]")为真实对象
{'x': <class 'int'>, 'return': <class 'str'>}# from __future__ import annotations(3.7+):标注变成字符串,惰性求值
# 好处 1:3.9 之前也能写 list[int]
# 好处 2:避免"类还没定义完就求值"(自引用标注)
# 代价:get_type_hints 需要运行时解析(慢、且字符串环境缺失时失败)
⚠️ 陷阱:
- 性能:
get_type_hints是运行时解析,调用有开销(毫秒级)——不要在热路径反复调用;需要时缓存结果;- 标注是普通属性:
f.__annotations__可被修改(元编程玩具,第 12 章);- 不加
from __future__ import annotations时,函数定义处就求值标注——def f(x: Undefined)会在定义时抛NameError(不是调用时);Any是"放弃检查"——过度使用等于没有类型标注;object才是"运行时是真的对象"(Any与object的语义差异:Any允许任意操作不报错,object只允许对象基础操作)。
本章精选了十大工具箱,但标准库还有一批"小而美"的模块——它们解决单点问题,即查即用(本系列"肘后备急"理念的集中体现):
| 模块 | 一句话定位 | 常用 API | 典型场景 |
|---|---|---|---|
hashlib |
哈希摘要 | hashlib.sha256(data).hexdigest() |
文件指纹、密码哈希(配 secrets) |
base64 |
二进制 ↔ 文本编码 | base64.b64encode(b"...") |
传输二进制(URL、邮件) |
uuid |
通用唯一标识 | uuid.uuid4().hex |
请求 ID、主键、幂等键 |
zipfile / tarfile |
归档文件 | zipfile.ZipFile(...).extractall() |
解压/打包(第 9 章 shutil 也有封装) |
pprint |
美化打印 | pprint.pprint(obj) |
调试复杂嵌套结构 |
difflib |
序列差异 | difflib.SequenceMatcher / unified_diff |
文本 diff、相似度匹配 |
webbrowser |
打开浏览器 | webbrowser.open(url) |
脚本"自动打开结果页" |
tempfile |
临时文件 | tempfile.NamedTemporaryFile(第 9 章讲过) |
安全临时文件 |
warnings |
弃用警告 | warnings.warn("...", DeprecationWarning) |
库 API 演进(第 10 章 10.4.1 用过) |
inspect |
对象内省 | inspect.getsource/signature/isfunction |
调试、框架、13.1.3 读源码 |
functools.reduce |
归约 | 见 13.4.4 | 聚合计算 |
contextlib |
上下文工具 | 第 8 章已细讲 | — |
dataclasses |
数据类 | 第 7 章已细讲 | — |
# 三个最高频的即查即用示例
>>> import hashlib, uuid, base64
>>> hashlib.sha256(b"data").hexdigest()[:16]
'3a6eb079...'
>>> uuid.uuid4()
UUID('7f9c2e8a-...')
>>> base64.b64encode(b"hello") # b'hello' → 文本
b'aGVsbG8='
>>> import difflib
>>> "".join(difflib.unified_diff(["a\n", "b\n"], ["a\n", "c\n"], lineterm=""))
'- b\n+ c\n'实战模式:新项目起步时的标准库"默认组合"——配置
tomllib+ 日志logging+ 参数argparse+ 存储sqlite3/json+ 哈希hashlib+ IDuuid+ 进程subprocess。这套组合能覆盖大多数中小工具的全部需求,依赖面为零(第 10 章锁文件/供应链视角的隐含红利)。等需求确实超出标准库(性能、领域功能)再引入第三方——13.1.2 的选型哲学在此落地。
| 主题 | 核心要点 |
|---|---|
| 标准库全景 | 313 个模块(sys.stdlib_module_names);六象限地图;选型:基础能力用标准库、核心路径可上第三方 |
| 读源码 | inspect.getsource();"C 加速 + Python 兜底"的双实现模式(_collections) |
collections |
defaultdict(__missing__)、Counter(C 计数)、OrderedDict(3.7+ 新定位)、deque(双端 O(1))、ChainMap、namedtuple、UserDict(安全子类化) |
itertools |
全部惰性;product 替嵌套循环、groupby 只认相邻、islice 惰性切片、tee 有缓存代价 |
functools |
partial(闭包显式化)、lru_cache(OrderedDict LRU)、singledispatch(注册表多态)、wraps(装饰器必加) |
datetime |
不可变 + 可哈希;timedelta 三字段归一化;内部 UTC、展示转本地;zoneinfo 替代 pytz(PEP 615);DST 折叠用 fold |
| 数字 | math.isclose 判浮点相等;货币用 Decimal(字符串构造 + localcontext);Fraction 精确有理数;安全随机用 secrets;fmean 精确累加 |
| 系统 | subprocess.run + shell=False 防注入;shutil.copy2 保留元数据;pathlib.glob 优先;sys.platform 做分支 |
| 持久化 | sqlite3 参数化防注入 + 3.12 with 事务;csv 必写 newline='';configparser 的 DEFAULT 继承;argparse 完整 CLI |
logging |
Logger/Handler/Formatter/Filter 四件套;getLogger(__name__) 层级;库只加 NullHandler;dictConfig + JSON 结构化 + 轮转 |
typing |
PEP 484 静态检查;3.9+ 内置泛型、3.10+ | 语法;TypeVar 泛型;只读容器协变安全;get_type_hints 有解析开销 |
第 1–3 题:验证理解(预测/解释)
-
预测输出:
Counter("aab") + Counter("abc")、Counter("aab") - Counter("abc")、Counter("aab") & Counter("abc")各是多少?defaultdict的d.get("missing")会触发默认值吗?为什么? -
解释
groupby的行为:list(groupby([1, 1, 2, 1, 2]))分成了几组?为什么不是"两个 1 一组、两个 2 一组"?要按全局键分组必须先做什么? -
解释
lru_cache装饰def load_config(path: str) -> dict会有什么隐患?Decimal(0.1)与Decimal("0.1")的区别是什么?为什么财务代码必须用后者?
第 4–6 题:动手实战
-
用
itertools.product重写一个三层嵌套循环(如生成所有(r, g, b)颜色组合),再用timeit对比嵌套循环版本与product版本的内存/耗时差异。 -
写一个"最近 N 条命令历史"的数据结构:用
deque(maxlen=N)实现;再对比list+pop(0)的实现,解释为什么deque更好。 -
用
zoneinfo写一个函数convert_time(iso_str, from_tz, to_tz) -> str:解析 ISO 时间戳 → 转目标时区 → 输出 ISO 格式。分别测试Asia/Shanghai→UTC和跨越 DST 的America/New_York日期。
第 7–9 题:实战进阶
-
用
argparse+sqlite3做一个迷你任务清单 CLI(add/list/done三个子命令),参数全部参数化查询,验证--help输出,并用[project.scripts](第 10 章)把它装成命令。 -
用
logging搭一个应用级日志:dictConfig配置(控制台 +RotatingFileHandler轮转 + JSON 结构化),子模块用getLogger(__name__),验证 propagate 行为,并演示"敏感字段脱敏"的Filter。 -
用
functools.lru_cache加速一个递归函数(如爬楼梯/组合数),打印cache_info()对比加速前后耗时;再用functools.singledispatch为int/str/list各写一个summarize(x)实现。
第 10 题:深度思考
- 假设你要为一个数据处理服务做技术选型:日志(标准库
loggingvs 第三方loguru)、配置(configparservstomllibvspydantic)、CLI(argparsevsclick)。基于 13.1.2 的选型矩阵,逐项给出你的决策与理由;再回答:标准库"自带电池"对项目的依赖面(第 10 章锁文件、供应链安全)有什么隐含价值?
进入下一章的准备:
- ✅ 能按"问题 → 模块"索引:计数用
Counter、组合用itertools、记忆化用lru_cache、时间用zoneinfo、金额用Decimal、进程用subprocess.run、参数用argparse、日志用logging - ✅ 能解释
collections双实现模式、deque的 O(1) 两端、groupby的相邻性、lru_cache的 LRU 机制 - ✅ 牢记安全红线:
shell=False、SQL 参数化、secrets而非random、日志脱敏 - ✅ 会读标准库源码(
inspect.getsource),知道"先查标准库再手写" - ✅ 能用
typing+ mypy 做静态检查,理解"标注默认不影响运行时"
下一章(第 14 章 测试与调试)将进入"如何证明你的代码是对的"——届时 logging 的调试价值、datetime 的可复现性(固定时间)、random.seed 的确定性,都是测试设计的关键工具,我们会回来引用本章。