Day18 · 函数(三)高阶函数、装饰器、函数注解与常用 BIF
Day18 · 函数(三)高阶函数、装饰器、函数注解与常用 BIF
阶段:阶段一 · 核心基础 | 前置:Day17 | 技术基准:Python 3.15.0
学习目标
- 掌握高阶函数
map/filter/reduce/sorted(key=...)的用法 - 理解装饰器(Decorator)原理,会写日志、计时装饰器,理解
functools.wraps - 会写带参数的装饰器、叠加多层装饰器
- 会用函数注解(类型提示),了解
str | None现代写法 - 熟悉常用内置函数 BIF:
callable/eval/exec/repr/abs/round/pow/divmod/enumerate/zip/all/any - 了解 Python 3.14/3.15 注解惰性求值(
annotationlib)、类型参数默认值(PEP 696)与map(strict=True)这三个和函数/注解/BIF 直接相关的新变化 - 会用
ast把源码解析成抽象语法树(ast.parse/ast.dump/ast.walk),理解ast与eval"只解析、不执行"的区别
一、高阶函数(Higher-order Function)
高阶函数就是"参数或返回值里包含函数"的函数。因为 Python 里函数是一等对象(Day16 讲过),所以可以把函数当参数传来传去。
1.1、map:逐个变换
map(函数, 可迭代对象) 把函数作用到每个元素上,返回一个迭代器(要用 list() 才能看到内容):
data = [1, 2, 3, 4, 5]
print(list(map(lambda x: x * 2, data))) # 输出:[2, 4, 6, 8, 10]
# 也可以传多个序列,按位置一一对应
print(list(map(lambda x, y: x + y, [1, 2], [10, 20]))) # 输出:[11, 22]
1.2、filter:按条件过滤
filter(函数, 可迭代对象) 保留函数返回 True 的元素:
data = [1, 2, 3, 4, 5, 6]
print(list(filter(lambda x: x % 2 == 0, data))) # 输出:[2, 4, 6]
1.3、reduce:累计运算
reduce 在 Python 3 里挪到了 functools 模块,需要先导入:
from functools import reduce
data = [1, 2, 3, 4, 5]
print(reduce(lambda x, y: x + y, data)) # 输出:15(相当于 1+2+3+4+5)
print(reduce(lambda x, y: x * y, data)) # 输出:120(5 的阶乘)
小嘉提醒:
map/filter现在很多时候可以用列表生成式[x*2 for x in data]替代,读起来更直观;但在面试、阅读老代码和函数式风格时一定会遇到。
1.4、sorted 的 key 参数
sorted(可迭代对象, key=函数) 用函数的返回值作为排序依据:
words = ["banana", "apple", "cherry"]
print(sorted(words, key=len)) # 按字符串长度排序
# 输出:['apple', 'banana', 'cherry']
points = [(1, 3), (2, 1), (4, 2)]
print(sorted(points, key=lambda p: p[1])) # 按第 2 个元素排序
# 输出:[(2, 1), (4, 2), (1, 3)]
二、装饰器(Decorator)
2.1、装饰器是什么
装饰器本质上是一个"接收函数、返回新函数"的高阶函数。它能在不修改原函数代码、也不改变原函数调用方式的前提下,给函数额外加功能(日志、计时、权限校验、缓存……)。这是设计模式里"开闭原则"的经典体现。
2.2、手写一个最简单的装饰器
def log(func):
def wrapper(*args, **kwargs):
print("开始执行:", func.__name__)
result = func(*args, **kwargs) # 真正调用原函数
print("执行结束:", func.__name__)
return result
return wrapper
def add(a, b):
return a + b
add = log(add) # 手动把 add 包装一遍
print(add(3, 5))
# 输出:
# 开始执行: add
# 执行结束: add
# 8
2.3、@ 语法糖
上面的 add = log(add) 可以直接写成在函数上面加一行 @log,这就是装饰器语法糖:
def log(func):
def wrapper(*args, **kwargs):
print("开始执行:", func.__name__)
result = func(*args, **kwargs)
print("执行结束:", func.__name__)
return result
return wrapper
@log # 等价于 add = log(add)
def add(a, b):
return a + b
print(add(3, 5)) # 输出:开始/结束日志 + 8
2.4、functools.wraps:保住原函数的"身份"
被装饰后,函数的名字会变成 wrapper,文档字符串也丢了。用 functools.wraps 可以把原函数的元信息复制过来:
from functools import wraps
def log(func):
@wraps(func) # 关键:把 func 的 __name__、__doc__ 复制给 wrapper
def wrapper(*args, **kwargs):
print("调用:", func.__name__)
return func(*args, **kwargs)
return wrapper
@log
def add(a, b):
"""返回两数之和。"""
return a + b
print(add.__name__) # 输出:add(不加 wraps 会是 wrapper)
print(add.__doc__) # 输出:返回两数之和。
2.5、实战:计时装饰器
import time
from functools import wraps
def cost_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
cost = time.perf_counter() - start
print(f"{func.__name__} 耗时 {cost:.6f} 秒")
return result
return wrapper
@cost_time
def total(n):
return sum(range(n))
print(total(1_000_000))
2.6、带参数的装饰器
如果想让装饰器自己也能接收参数(比如自定义日志级别),就需要再多包一层:
from functools import wraps
def repeat(times): # 这一层用来接收装饰器参数
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3) # 装饰器本身传了参数 3
def greet(name):
print("你好,", name)
greet("小嘉") # 打印 3 次"你好, 小嘉"
2.7、多层装饰器叠加
一个函数上可以叠多个装饰器,离函数最近的先包装,执行时最上面的先进入:
@log
@cost_time
def work():
print("工作中")
# 等价于 work = log(cost_time(work))
工程里还有很多现成装饰器:@staticmethod、@classmethod、@property(Day19/19 讲),以及 Flask/FastAPI 的路由装饰器。
三、函数注解(Function Annotations)
3.1、基本写法
函数注解在参数后面写 : 类型,在参数列表后面写 -> 返回类型。它只是给人看、给 IDE 提示的,不会强制做类型检查:
def add(a: int, b: int) -> int:
"""返回两个整数的和"""
return a + b
print(add(3, 5)) # 输出:8
print(add.__annotations__)
# 输出:{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
# 注解只是提示,传别的类型也不报错
print(add("a", "b")) # 输出:ab
3.2、现代类型提示写法
Python 3.10+ 支持 PEP 604 的 | 写法,3.15 完全支持。表示"可能是 None"直接用 str | None,不用再写 Optional[str]:
def greet(name: str, age: int | None = None) -> str:
if age is None:
return f"你好,{name}"
return f"你好,{name},今年 {age} 岁"
print(greet("小嘉")) # 输出:你好,小嘉
print(greet("小嘉", 18)) # 输出:你好,小嘉,今年 18 岁
需要更复杂的类型(列表、字典里装什么)时,可以从 typing 模块或内置泛型写起:
def average(nums: list[float]) -> float:
return sum(nums) / len(nums)
print(average([1.5, 2.5, 3.0])) # 输出:2.3333333333333335
四、常用内置函数 BIF
Python 的 BIF(Built-in Function)非常多,用 dir(__builtins__) 可以看全列表。这里挑高频常用的讲。
4.1、callable:能否被调用
def test():
pass
num = 10
print(callable(test)) # 输出:True(函数可调用)
print(callable(print)) # 输出:True
print(callable(num)) # 输出:False(数字不可调用)
print(callable("hello")) # 输出:False
4.2、repr:对象的"官方字符串"
str() 偏给人看,repr() 偏给解释器看(能看清类型和结构):
s = "hello"
print(str(s)) # 输出:hello
print(repr(s)) # 输出:'hello'(带引号,能看出是字符串)
4.3、abs / round / pow / divmod
print(abs(-10)) # 输出:10
print(abs(3 + 4j)) # 输出:5.0(复数的模)
print(round(3.14159, 2)) # 输出:3.14
print(round(2.5)) # 输出:2(银行家舍入:.5 向偶数靠拢)
print(pow(2, 10)) # 输出:1024
print(pow(2, 10, 100)) # 输出:24(等价 (2**10) % 100,做加密时更快)
print(divmod(10, 3)) # 输出:(3, 1)(同时给商和余数)
小嘉提醒:涉及金额等精确计算时别依赖
round()(浮点有精度问题),请用decimal模块。
4.4、enumerate 与 zip
# enumerate:同时拿到下标和元素
for i, ch in enumerate("abc"):
print(i, ch)
# 输出:0 a / 1 b / 2 c
# zip:把多个序列按位置打包
names = ["小嘉", "阿明", "小红"]
scores = [90, 85, 95]
for n, s in zip(names, scores):
print(n, s)
# 输出:小嘉 90 / 阿明 85 / 小红 95
4.5、all 与 any
print(all([True, 1, "x"])) # 输出:True(全部为真)
print(all([True, 0, "x"])) # 输出:False(有一个假)
print(any([0, "", "x"])) # 输出:True(至少一个真)
4.6、eval / exec / compile(注意安全!)
eval 把字符串当表达式执行并返回结果;exec 执行多行代码但不返回值。它们能执行任意代码,千万不要对不可信的字符串使用:
print(eval("1 + 2 * 3")) # 输出:7
print(eval("[1, 2, 3]")) # 输出:[1, 2, 3]
code = "for i in range(3):\n print('exec 执行:', i)"
exec(code)
# 输出:exec 执行: 0 / 1 / 2
# compile 可以先把字符串编译成代码对象,重复执行避免反复编译
c = compile("1 + 2 * 3", "<string>", "eval")
print(eval(c)) # 输出:7
更安全的替代是 ast.literal_eval()——它只能解析列表/字典等字面量,不能执行代码:
import ast
print(ast.literal_eval("{'name': '小嘉', 'age': 18}"))
# 输出:{'name': '小嘉', 'age': 18}
4.7、AST:把代码当数据看(ast 模块)
ast(Abstract Syntax Tree,抽象语法树)是标准库,能把 Python 源码解析成一棵结构树——每个表达式、语句都变成树上的节点。写静态分析工具(代码检查、格式化、提取变量名)、做安全审查时很有用(本机 py -3 实测):
import ast
# 1) ast.parse:把源码字符串解析成 AST 树(根节点是 Module)
tree = ast.parse("x = 1 + 2\nprint(x)")
print(type(tree).__name__) # Module
# 2) ast.dump:把树结构打印出来,看懂节点长什么样(indent 参数 3.9+ 支持)
print(ast.dump(tree, indent=2))
ast.dump 的真实输出:
Module(
body=[
Assign(
targets=[
Name(id='x', ctx=Store())],
value=BinOp(
left=Constant(value=1),
op=Add(),
right=Constant(value=2))),
Expr(
value=Call(
func=Name(id='print'),
args=[
Name(id='x')]))])
可以看到 x = 1 + 2 被拆成 Assign(赋值)、Name(变量名)、BinOp(二元运算,Add 加法)、Constant(常量)。遍历这棵树就能"读懂"代码:
import ast
tree = ast.parse("x = 1 + 2\nprint(x)")
# 3) ast.walk:深度优先遍历所有节点
print([n.__class__.__name__ for n in ast.walk(tree)])
# ['Module', 'Assign', 'Expr', 'Name', 'BinOp', 'Call', 'Store', 'Constant', 'Add', ...]
# 4) 提取源码里所有的变量名(Name 节点)
names = [n.id for n in ast.walk(tree) if isinstance(n, ast.Name)]
print(names) # ['x', 'print', 'x']
# 5) 安全求值字面量(复习 4.6 节)
print(ast.literal_eval("{'name': '小嘉', 'age': 18}"))
# {'name': '小嘉', 'age': 18}
小嘉说明:
ast与eval的关键区别——eval会执行代码,ast只解析代码不动手。正因如此,安全做法是用ast.parse先解析、检查节点,再决定要不要执行;ast.literal_eval更是只认"字面量"、不执行任何代码。写代码分析工具(如统计某文件用了哪些函数名)时,ast是第一选择。
3.15、类型与注解新视角(Python 3.14 / 3.15)
前面讲的函数注解、BIF、map 都是"基本功"。Python 3.14 / 3.15 在类型与注解上又悄悄变了天。本节把和"函数、注解、内置函数"最直接相关的三个新变化捡出来讲,让你以后看新代码、读库源码时不懵。(f-string、容错解析器等其他新能力在"Python 新特性"专题天统一讲,这里不展开。)
3.15.1、注解惰性求值(PEP 649 / 749)
过去的老规矩:函数/类的注解在"定义那一刻"就被立刻求值。只要注解里写了一个还没 import 进来的名字(前向引用、循环 import),模块一加载就炸 NameError。所以历史上大家被迫在文件头写 from __future__ import annotations,把所有注解一股脑转成字符串来躲坑。
现在(3.14 实现、3.15 起成为默认行为):注解默认不再立即求值,而是被存成一个"待求值"的 blob。你不碰它,它就不报错;真要拿到类型对象时,再按需求值。
但要注意一个坑——你要是直接去读 func.__annotations__,它为了还给你一个"老格式"的字典,会立刻触发求值,未定义的名字照样抛 NameError:
import annotationlib
from annotationlib import Format
def bad(x: UndefinedName) -> int: # UndefinedName 此时还不存在
return 1
try:
print(bad.__annotations__)
except NameError as e:
print("NameError:", e)
# 输出:NameError: name 'UndefinedName' is not defined
想要"按需取用",请用标准库新模块 annotationlib,它的 get_annotations(对象, format=...) 支持三种格式:
def ok(name: str, age: int) -> list[str]:
return []
# VALUE:求值后的真实对象(和旧的 typing.get_type_hints 接近)
print(annotationlib.get_annotations(ok, format=Format.VALUE))
# 输出:{'name': <class 'str'>, 'age': <class 'int'>, 'return': list[str]}
# STRING:只给源码字符串,绝不求值——最安全,前向引用/循环 import 也不怕
print(annotationlib.get_annotations(ok, format=Format.STRING))
# 输出:{'name': 'str', 'age': 'int', 'return': 'list[str]'}
# FORWARDREF:每个注解包成一个 ForwardRef 惰性引用,要用时再 evaluate()
fr = annotationlib.get_annotations(bad, format=Format.FORWARDREF)
print({k: type(v).__name__ for k, v in fr.items()})
# 输出:{'x': 'ForwardRef', 'return': 'type'}
try:
fr["x"].evaluate() # 主动求值,此时 UndefinedName 仍未定义
except NameError as e:
print("evaluate() -> NameError:", e)
# 输出:evaluate() -> NameError: name 'UndefinedName' is not defined
小嘉提醒:正因为注解默认惰性求值了,"注解引用后面才定义的类"这种前向引用、以及模块间循环 import,都不再需要
from __future__ import annotations兜底了。写库、写框架时,优先用annotationlib按需取注解,而不是直接读__annotations__。
3.15.2、类型参数的默认值(PEP 696,3.13+)
以前写泛型,类型参数"传几个就得写几个"。PEP 696 允许给 TypeVar / ParamSpec 设默认值,调用方不传时自动回退到默认:
from typing import TypeVar, Generic, ParamSpec
K = TypeVar("K")
V = TypeVar("V", default=str) # V 有默认值 str
class Pair(Generic[K, V]):
pass
# 只传 K,V 自动回退到默认 str
print(Pair[int])
# 输出:__main__.Pair[int, str]
# 显式传两个,则覆盖默认
print(Pair[int, int])
# 输出:__main__.Pair[int, int]
# 默认值本身也能从类型参数上读出来
T = TypeVar("T", default=int)
print(T.__default__)
# 输出:<class 'int'>
P = ParamSpec("P", default=())
print(P.__default__)
# 输出:()
配合 PEP 695 的新泛型函数语法(def 名字[T](参数...),3.12+),写泛型函数顺手很多:
def first[T](xs: list[T]) -> T:
return xs[0]
print(first([1, 2, 3])) # 输出:1
print(first(["a", "b"])) # 输出:a
3.15.3、map() 的 strict 参数(PEP 618,3.11+)
第一节讲过 map 可以一次传多个可迭代对象,按位置一一对应。老版本里如果这些对象长度不一致,map 会静默截断到最短,很容易埋雷。Python 3.11+ 给 map 加了 strict 参数:传 strict=True 时,长度不一致就立刻抛 ValueError,不再悄悄截断:
# 等长:正常
print(list(map(lambda a, b: a + b, [1, 2], [10, 20], strict=True)))
# 输出:[11, 22]
# 不等长 strict=True:立刻报错
try:
list(map(lambda a, b: a + b, [1, 2, 3], [10, 20], strict=True))
except ValueError as e:
print("ValueError:", e)
# 输出:ValueError: map() argument 2 is shorter than argument 1
# 不传 strict:老行为,静默截断到最短([1,2] 配 [10,20])
print(list(map(lambda a, b: a + b, [1, 2, 3], [10, 20])))
# 输出:[11, 22]
小嘉提醒:处理"必须一一对齐"的数据(比如姓名表和成绩表两列)时,养成加
strict=True的习惯——长度对不齐当场崩,好过线上算错了没人发现。
五、装饰器实战:带缓存的递归
Day17 的朴素斐波那契因为重复计算很慢。用一个"缓存装饰器"就能瞬间提速——这正是装饰器"不改原函数、只加能力"的威力:
from functools import wraps
def memoize(func):
cache = {} # 闭包字典,记住算过的参数
@wraps(func)
def wrapper(n):
if n not in cache:
cache[n] = func(n) # 没算过才真算
return cache[n]
return wrapper
@memoize
def fib(n):
if n <= 2:
return 1
return fib(n - 1) + fib(n - 2)
print(fib(100)) # 输出:354224848179261915075(不加缓存根本跑不动)
其实标准库已经内置了这个装饰器:functools.cache / functools.lru_cache,实际项目直接用它们即可:
from functools import lru_cache
@lru_cache(maxsize=None)
def fib2(n):
return 1 if n <= 2 else fib2(n - 1) + fib2(n - 2)
print(fib2(50)) # 输出:12586269025
六、map/filter 与列表生成式怎么选
很多场景下 map/filter 都能被列表生成式替代,而且后者往往更直观:
data = [1, 2, 3, 4, 5, 6]
# map 写法
print(list(map(lambda x: x * x, data))) # [1, 4, 9, 16, 25, 36]
# 列表生成式写法(更推荐,一眼看懂)
print([x * x for x in data]) # [1, 4, 9, 16, 25, 36]
# filter 写法
print(list(filter(lambda x: x % 2 == 0, data))) # [2, 4, 6]
# 列表生成式带条件
print([x for x in data if x % 2 == 0]) # [2, 4, 6]
小嘉的建议:写新代码优先用列表生成式(Python 之禅式的"只有一种显而易见的做法");阅读老代码、面试题里遇到 map/filter/reduce 时能认出来、能看懂就行。
七、functools 与 operator:函数式编程的标准库搭档
前面用的 map/filter/reduce/装饰器,很多"标准操作"标准库都帮你封装好了,最常见的两个就是 functools(面向函数)和 operator(面向运算符)。它们能让你少写 lambda,代码更短更清晰。以下示例均在 Python 3.13 实测跑通。
7.1、functools:操作函数的工具箱
① partial:固定部分参数,造一个"专用版"函数(Day17 讲过,这里再和本模块归到一起)
from functools import partial
def power(x, n): return x ** n
square = partial(power, n=2) # 固定 n=2
print(square(5)) # 25,等价于 power(5, 2)
② lru_cache / cache:记忆化缓存,避免重复计算(第五节"带缓存的递归"就用它)
from functools import cache
@cache # 简单的永久缓存;有容量限制用 lru_cache(maxsize=128)
def fib(n): return n if n < 2 else fib(n-1) + fib(n-2)
print(fib(10)) # 55
③ cached_property:类里"只算一次"的属性(算完缓存,下次直接取)
from functools import cached_property
class Circle:
def __init__(self, r): self.r = r
@cached_property
def area(self): return 3.14159 * self.r ** 2 # 只执行一次
c = Circle(2)
print(c.area) # 12.56636
print(c.area) # 第二次直接读缓存,不再计算
④ total_ordering:只写 __eq__ 和 __lt__,其余比较自动补齐(<、<=、>、>=、!= 全都能用)
from functools import total_ordering
@total_ordering
class Person:
def __init__(self, name, age): self.name, self.age = name, age
def __eq__(self, o): return self.age == o.age
def __lt__(self, o): return self.age < o.age
p1, p2 = Person("小嘉", 20), Person("阿豆", 25)
print(p1 < p2, p1 <= p2, p1 != p2) # True True True
⑤ singledispatch:按"第一个参数的类型"分派到不同实现(写一个多态函数)
from functools import singledispatch
@singledispatch
def show(x): return f"通用: {x}" # 默认实现
@show.register(int)
def _(x): return f"整数: {x}"
@show.register(str)
def _(x): return f"字符串: {x}"
print(show(1), show("hi"), show([1, 2]))
# 整数: 1 字符串: hi 通用: [1, 2]
小嘉提醒:
functools里reduce(1.3)、wraps(2.4)、lru_cache(五)前面已经用过,这一节把它们和partial/cached_property/total_ordering/singledispatch归拢到一起,形成一个"函数工具"速查表。日常最常用的是partial(固定参数)和lru_cache(缓存),先用熟这两个就够。
7.2、operator:把运算符变成函数
operator 把 + - * / == < > 等运算符包装成函数,好处是可以直接当 map/reduce/sorted(key=...) 的参数,不用写 lambda。
① 运算符函数:add/sub/mul、lt/gt/eq、truth/not_ 等
import operator
from functools import reduce
print(operator.add(2, 3)) # 5,等价于 2 + 3
print(operator.lt(2, 3)) # True,等价于 2 < 3
print(reduce(operator.add, [1, 2, 3, 4])) # 10,等价于 reduce(lambda a,b: a+b, ...)
② itemgetter:按"键名或下标"取值,常用来给 sorted/max/min 当排序依据
students = [{"name": "张三", "score": 88}, {"name": "李四", "score": 92}]
lowest = min(students, key=operator.itemgetter("score"))
print(lowest["name"]) # 张三(分数最低)
③ attrgetter:按"属性名"取值,对对象列表同样适用
ps = [...] # 一堆带 .age 的对象
oldest = max(ps, key=operator.attrgetter("age"))
④ methodcaller:调用对象的方法
to_upper = operator.methodcaller("upper")
print(to_upper("hi")) # HI
⑤ 新成员 is_none / is_not_none(Python 3.14+ 新增):等价于 obj is None / obj is not None,可直接用作过滤条件。这是 Day27 讲过的 3.14/3.15 新特性,此处归位存档。
小嘉提醒:
operator的定位是"用现成函数代替 lambda"。当你要写key=lambda x: x["score"]时,先想想能不能用itemgetter("score");要写lambda a, b: a + b时,先想想operator.add。代码更短、也更快。
7.3、顺便认识:itertools(第三个函数式搭档)
除了 functools、operator,标准库里函数式风格最常用的还有 itertools——提供"按规则生成迭代序列"的工具(无限循环 cycle、排列组合 product/permutations、分组 groupby、拼接 chain 等)。它和 map/filter/reduce 天然搭配,等学到 Day15 迭代器章节时再展开,这里先知道名字即可。
八、typing:类型提示的标准库工具箱
第三节讲了函数注解的基本写法(x: int -> int、str | None),但"类型提示"本身是一个完整的子系统,核心就在 typing 模块。日常写业务注解时,list[int]、dict[str, int] 这种内置泛型够用;一旦要写通用的数据结构、跨模块的接口约定、给第三方库的类型,就离不开下面这些"工具箱"。以下示例均在本机 Python 3.13 实测。
8.1、TypeVar 与 Generic:写"通用"数据结构
TypeVar 声明一个"占位类型",Generic[T] 让类成为泛型类——同一个类可以装任意类型,但装进去什么、取出来就是什么,类型不丢:
from typing import TypeVar, Generic
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self):
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
s: Stack[int] = Stack() # 声明这是一个"装 int 的栈"
s.push(1); s.push(2)
print(s.pop(), s.pop()) # 输出:2 1
小嘉提醒:3.12 起可以用 PEP 695 的新语法
class Stack[T]:直接省掉TypeVar/Generic(3.15.2 节演示过泛型函数def first[T](xs: list[T]));但读老库源码时TypeVar+Generic仍是主流写法,必须认得。
8.2、Protocol 与 @runtime_checkable:鸭子类型的显式约定
Python 是鸭子类型——"长得像鸭子、叫得像鸭子就当鸭子"。Protocol 把这种约定写成显式的"接口":只要一个类实现了约定的方法,就算符合协议,不需要显式继承。
from typing import Protocol, runtime_checkable
@runtime_checkable # 加了它才能用 isinstance() 做运行时检查
class SupportsClose(Protocol):
def close(self) -> None: ...
class FileLike: # 没有继承 SupportsClose
def close(self) -> None:
print("close!")
print(isinstance(FileLike(), SupportsClose)) # 输出:True(因为它有 close 方法)
runtime_checkable 只检查"方法在不在",不检查方法签名。它和 collections.abc 里的 Iterable/Iterator/Sized 是同一类思路(结构化 vs 名义子类型),只是 collections.abc 面向容器协议,typing.Protocol 面向你自定义的任意接口。
8.3、NewType:给基本类型套一层"类型别名"防混用
UserId = NewType("UserId", int) 造出一个"表面是 int、实则是新类型"的对象。它运行时就是 int(零开销),但类型检查器会把它和普通 int 区分开——避免把"金额 100 块"和"用户 ID 1001"搞混:
from typing import NewType
UserId = NewType("UserId", int)
uid = UserId(1001)
print(uid, type(uid)) # 输出:1001 <class 'int'>(运行时仍是 int)
8.4、Literal:限定取值必须是某几个字面量之一
from typing import Literal
def set_mode(mode: Literal["r", "w", "a"]) -> str:
return f"打开模式 {mode}"
print(set_mode("r")) # 输出:打开模式 r
# set_mode("x") # 类型检查器会报错:"x" 不在允许的字面量里
8.5、TypedDict:给字典的"形状"定型
普通 dict[str, object] 说不清"这个字典到底有哪些键、每个键是什么类型"。TypedDict 专门描述"固定键、固定值类型"的字典(处理 JSON 接口返回时极常用):
from typing import TypedDict
class User(TypedDict):
name: str
age: int
u: User = {"name": "小嘉", "age": 18}
print(u["name"], u["age"]) # 输出:小嘉 18
8.6、ParamSpec 与 Concatenate:给"装饰器"保留签名
8.1 的泛型解决"类装什么",ParamSpec(PEP 612,3.10+)解决"装饰器怎么原样保住原函数的参数签名"。P.args/P.kwargs 把被装饰函数的参数整体转发:
from typing import Callable, ParamSpec
P = ParamSpec("P")
def deco(func: Callable[P, object]) -> Callable[P, str]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> str:
return "result:" + str(func(*args, **kwargs))
return wrapper
@deco
def add(a: int, b: int) -> int:
return a + b
print(add(1, 2)) # 输出:result:3
Concatenate[原参数..., 新插入的参数] 则用于"在装饰器里往参数列表最前面再塞一个参数"的场景,写框架时才会用到,知道名字即可。
8.7、TypeAlias、final、overload:三个实用小工具
from typing import TypeAlias, final, overload
# ① TypeAlias(3.10+):给一长串类型起个可读的别名
Vector: TypeAlias = list[float]
def scale(v: Vector, k: float) -> Vector:
return [x * k for x in v]
print(scale([1.0, 2.0], 2.0)) # 输出:[2.0, 4.0]
# ② @final:声明"这个类/方法不许被继承/重写"
@final
class Base:
pass
# class Derived(Base): ... # 类型检查器(mypy 等)会报错
# 注意:@final 在 Python 运行时【不强制执行】,它只是给类型检查器看的提示
# ③ @overload:同一个函数按参数不同返回不同类型
@overload
def process(x: int) -> int: ...
@overload
def process(x: str) -> str: ...
def process(x):
return x + 1 if isinstance(x, int) else x.upper()
print(process(1), process("hi")) # 输出:2 HI
小嘉提醒:
@overload写的多个重载签名只给类型检查器看,真正的实现函数(最后那个不带@overload的)才是运行时执行的代码;@overload本身运行时就是空函数,别把逻辑写进它。
8.8、常用形式速查:Callable / Iterable 等
注解里最常见的"容器/可调用"写法,现代版本优先用内置泛型(list[int]、collections.abc.Iterable),但读老代码会遇到 typing 版:
from typing import Callable, Iterable
def apply(fn: Callable[[int], int], data: Iterable[int]) -> list[int]:
# Callable[[参数类型...], 返回类型];Iterable[int] = 可迭代的 int
return [fn(x) for x in data]
print(apply(lambda x: x * x, [1, 2, 3])) # 输出:[1, 4, 9]
8.9、3.15 视角:TypeForm(PEP 747)与 TypedDict 的 closed/extra_items(PEP 728)
本机探测(Python 3.13.16):
hasattr(typing, "TypeForm")为False;给 TypedDict 传closed=True或extra_items=int直接抛TypeError: _TypedDictMeta.__new__() got an unexpected keyword argument 'closed'/'extra_items'。这两个都是 3.15 新增,本机 3.13.16 无法实测,下面按官方文档编写。
8.5 节用 TypedDict 给字典"定型"。3.15 又补了两个和"类型本身怎么标注"相关的小工具:
① typing.TypeForm[T](PEP 747)——"类型表达式"的注解类型
以前写泛型工厂函数时,参数"到底要传一个类(类型),还是传一个实例",注解里很难说清。TypeForm[T] 就是专门注解"这里传的是一个类型(类型表达式),而不是该类型的实例":
# 仅 Python 3.15+ 可运行
from typing import TypeForm
class Base: ...
class Derived(Base): ...
def new_instance(t: TypeForm[Base]):
# t 传进来的是 Base 的某个子类(如 Derived),不是 Base 的实例
return t()
# TypeForm(x) 在运行时原样返回 x,零开销(不做任何包装/校验)
print(TypeForm(int) is int) # 输出:True
它解决的是"类型工厂函数"的注解表达:type[X] 只能说"传一个类对象",而 TypeForm[X] 进一步说"传一个类型表达式,类型检查器会校验它确实是 X 的子类型"。
② TypedDict 的 closed 与 extra_items(PEP 728)——堵住/放开"多余的键"
8.5 节的 TypedDict 默认是"开放"的:类型检查器只要求你声明的键在,多传一个没声明的键它并不报错。3.15 给它加了两个类参数:
closed=True:封闭——多传任何一个没声明的键都报错(防 typo、防偷偷塞字段);extra_items=类型:开放但受控——允许任意额外键,但所有额外键的值都必须是指定类型(常见于"主字段固定、还带一堆自定义 metadata"的 JSON)。
# 仅 Python 3.15+ 可运行
from typing import TypedDict
class User(TypedDict, closed=True): # 封闭:只准 name、age
name: str
age: int
# u: User = {"name": "小嘉", "age": 18, "nam": "typo"}
# 类型检查器报错:多余键 "nam"(拼错了也能当场被抓)
class Metric(TypedDict, extra_items=float): # 开放但受控:必带 ts,其余键的值都得是 float
ts: float
小嘉提醒:
closed适合"接口入参必须严格按契约"的场景(多一个字段都要报警);extra_items适合"主结构固定 + 动态扩展字段"的场景(如配置、JSON 元数据)。两者都只是给类型检查器看的约定,运行时 TypedDict 依旧是普通 dict,不做任何校验。
九、contextlib:上下文管理器工具箱
with 语句 Day19 讲文件时会细讲(open() 就是最典型的上下文管理器)。contextlib 这个标准库模块提供的是"自己动手写、组合上下文管理器"的工具。以下示例本机 3.13 实测。
9.1、@contextmanager:用生成器手写上下文管理器
不用写一个带 __enter__/__exit__ 的类,用一个生成器函数 + @contextmanager 就能搞定——yield 之前是"进入 with",yield 的值就是 as 后面拿到的对象,yield 之后是"退出 with":
import time
from contextlib import contextmanager
@contextmanager
def timer(tag):
start = time.perf_counter()
yield "计时中" # 把这个值交给 as 后面的变量
print(f"[{tag}] 用时 {time.perf_counter() - start:.4f} 秒")
with timer("测试") as t:
print("在 with 块里:", t)
# 输出:
# 在 with 块里: 计时中
# [测试] 用时 0.0000 秒
9.2、ExitStack:动态、数量不定地叠多个上下文
如果要打开的文件个数是运行时才知道的(比如处理用户传入的 N 个文件),没法预先写 N 个 with。ExitStack 就是一个"上下文管理器的栈",把一个个 enter_context 进去,离开时按进入的逆序自动全部关闭:
from contextlib import contextmanager, ExitStack
@contextmanager
def opener(name):
print(f"打开 {name}")
yield name
print(f"关闭 {name}")
with ExitStack() as stack:
files = [stack.enter_context(opener(f"文件{i}")) for i in range(3)]
print("处理:", files)
# 输出(注意关闭是逆序的:文件2 -> 文件1 -> 文件0):
# 打开 文件0 / 打开 文件1 / 打开 文件2
# 处理: ['文件0', '文件1', '文件2']
# 关闭 文件2 / 关闭 文件1 / 关闭 文件0
9.3、suppress:优雅地忽略指定异常
from contextlib import suppress
with suppress(ZeroDivisionError): # 只吞 ZeroDivisionError,别的异常照常抛出
1 / 0
print("这行不会执行")
print("异常被静默吞掉,程序继续")
小嘉提醒:
suppress(具体异常)比try: ... except: pass安全得多——它只吞你明确列出的异常,不会把KeyboardInterrupt、NameError之类真正的 bug 一起盖掉。
9.4、redirect_stdout / redirect_stderr:临时改"输出去哪了"
写测试、捕获程序打印时特别有用——把本来该打到屏幕的 print 临时导向一个内存缓冲区:
import io
from contextlib import redirect_stdout
buf = io.StringIO()
with redirect_stdout(buf):
print("这段话不会出现在屏幕,进缓冲区了")
print("捕获到的内容:", repr(buf.getvalue()))
# 输出:捕获到的内容: '这段话不会出现在屏幕,进缓冲区了\n'
9.5、nullcontext 与 ContextDecorator、asynccontextmanager
from contextlib import nullcontext, ContextDecorator
# ① nullcontext:没有上下文管理器可用时的"空占位",with 块照常进,as 拿到你给的默认值
cm = nullcontext("默认值")
with cm as x:
print("拿到:", x) # 输出:拿到: 默认值
# ② ContextDecorator:把一个上下文管理器同时变成装饰器(@my_cm 用在函数上)
# ③ asynccontextmanager:给异步(async with)用的生成器版上下文管理器,FastAPI/异步爬虫里常见
nullcontext 常见于"有时需要加锁/有时不需要"的分支:
def maybe_open(do_open):
cm = opener("可选文件") if do_open else nullcontext("默认值")
with cm as x:
print("拿到:", x)
9.6、3.15 视角:ExitStack 接纳任意描述符、装饰器版上下文跨迭代保活
本机探测(Python 3.13.16):
ExitStack().enter_context(object())直接抛TypeError: 'builtins.object' object does not support the context manager protocol——3.13 只接收"已经实现__enter__/__exit__的上下文管理器"。下面 3.15 的放宽行为本机无法实测,按官方文档编写。
9.2 节的 ExitStack 在 3.15 做了两处放宽:
ExitStack/AsyncExitStack现在支持任意描述符:不再要求传入对象必须完整实现__enter__/__exit__(异步侧对应__aenter__/__aexit__)。只要它是一个"描述符风格"的对象,就能被enter_context收进栈里统一管理退出时机——写资源管理框架、组合第三方对象时,少写很多"外面再包一层 CM"的胶水代码。ContextDecorator/@contextmanager/@asynccontextmanager作为装饰器时:现在能识别被装饰对象是生成器函数还是协程函数,并在多次调用(跨迭代/跨请求)之间正确保持各自的上下文状态——不会把上一次的 yield 状态串到下一次调用里。
# 仅 Python 3.15+ 可运行:3.15 起 enter_context 可接收"描述符风格"对象
from contextlib import ExitStack
class MaybeResource: # 不再需要严格实现 __enter__/__exit__ 协议
def __get__(self, obj, objtype=None):
...
with ExitStack() as stack:
stack.enter_context(MaybeResource()) # 3.15 可接受;3.13 会抛 TypeError
小嘉提醒:这条变化日常写业务代码感知不强,但读 3.15 时代的库源码会看到"
enter_context(第三方对象)"这种以前必须自己包一层@contextmanager才能干的事——它反映的是标准库在"把更多对象都看成可组合的资源"这个方向上的持续放宽。
十、annotationlib:注解求值 API 深入(衔接 3.15.1)
3.14+ 新增模块(本机为 Python 3.13,没有
annotationlib,无法实测;本节按官方文档与 PEP 649/749 编写,供升级到 3.14/3.15 后对照)。
3.15.1 节已经用 annotationlib.get_annotations(obj, format=...) 实测过三种格式的基本效果。这里把它的"底层引擎"和"三种格式怎么取舍"讲透。
10.1、底层引擎:函数/类上的 __annotate__
PEP 649 之后,注解不再存成一张立刻求值的字典,而是变成对象上一个特殊方法 __annotate__(format)。你平时读的 func.__annotations__,其内部实现其实就是去调 __annotate__(<旧格式>) 并拼出老字典——所以一读 __annotations__ 就触发求值,遇到前向引用里还没定义的名字照样 NameError(这就是 3.15.1 节那个 bad(x: UndefinedName) 会炸的原因)。
annotationlib 只是对 __annotate__ 的一层友好封装。
10.2、三格式(VALUE / FORWARDREF / STRING)怎么选
| 格式 | 得到什么 | 何时用 |
|---|---|---|
Format.VALUE |
求值后的真实类型对象(接近旧 typing.get_type_hints) |
真要拿类型去做运行时检查、注册、序列化时 |
Format.STRING |
注解的源码字符串,绝不求值 | 做静态分析、文档生成、给 LLM 喂代码;有前向引用/循环 import 也不怕 |
Format.FORWARDREF |
每个注解包成一个 ForwardRef,需要时再 .evaluate() |
想自己控制"何时、在什么命名空间下"求值 |
按官方文档的示例(3.14+ 环境下运行):
import annotationlib
from annotationlib import Format
def ok(name: str, age: int) -> list[str]:
return []
# VALUE:求值后的真实对象
print(annotationlib.get_annotations(ok, format=Format.VALUE))
# {'name': <class 'str'>, 'age': <class 'int'>, 'return': list[str]}
# STRING:只给源码字符串,最安全
print(annotationlib.get_annotations(ok, format=Format.STRING))
# {'name': 'str', 'age': 'int', 'return': 'list[str]'}
# FORWARDREF:惰性引用,用时再 evaluate()
fr = annotationlib.get_annotations(ok, format=Format.FORWARDREF)
print(type(fr["name"]).__name__) # ForwardRef
小嘉提醒:写库、写框架时,优先
Format.STRING(只读不执行) 来收集注解做文档/校验;只有真的要把注解当类型对象用(如运行时校验字段类型),才用VALUE。不要图省事直接读__annotations__——它会立刻求值,把"本可以懒着不报错"的前向引用变成运行时崩溃。
十一、ast 实战:literal_eval 安全求值与 NodeTransformer
4.7 节已经讲过 ast.parse/ast.dump/ast.walk 这套"把代码解析成树、再遍历"的基本功。这里补两个能直接落地的实战能力:安全求值字面量,以及改写代码树。本机 3.13 实测。
11.1、ast.literal_eval:只认字面量的安全求值
4.6 节说过 eval(用户输入) 危险。ast.literal_eval 是它的"安全版"——只接受 Python 字面量(字典、列表、元组、字符串、数字、True/False/None、集合),遇到函数调用、运算符、__import__ 一律拒绝:
import ast
print(ast.literal_eval("{'name': '小嘉', 'age': 18, 'score': [90, 95]}"))
# 输出:{'name': '小嘉', 'age': 18, 'score': [90, 95]}
# 危险代码:直接抛 ValueError,绝不执行
try:
ast.literal_eval("__import__('os').system('dir')")
except ValueError as e:
print("ValueError:", e)
# 输出:ValueError: malformed node or string on line 1: <ast.Call object ...>
小嘉提醒:配置文件里那种"长得像字典"的字符串、网络传来的 JSON 替代品,一律用
literal_eval而不是eval。它解析的是"数据",不是"代码"。
11.2、NodeTransformer:遍历并改写 AST
ast.walk 只读不改;ast.NodeTransformer 是一个会就地替换节点的遍历器——重写它的 visit_节点类型 方法,就能把某种节点换成另一种。下面这个例子把源码里所有的"加法 +"偷偷换成"乘法 *":
import ast
class MultiplyForAdd(ast.NodeTransformer):
def visit_Add(self, node): # 凡是遇到加法节点
return ast.Mult() # 就替换成乘法节点
tree = ast.parse("result = 1 + 2 + 3")
new_tree = MultiplyForAdd().visit(tree) # 改树
ast.fix_missing_locations(new_tree) # 必须:给新节点补上源码位置信息
exec(compile(new_tree, "<改后>", "exec"))
print("1+2+3 被改算成 1*2*3 =", result)
# 输出:1+2+3 被改算成 1*2*3 = 6
写完 visit 后一定要调 ast.fix_missing_locations(新树),否则新节点没有行号信息,compile 时会报错。代码格式化工具(如 black)、自动加日志的插桩工具、把旧式类型注解批量改成新式写法的迁移脚本,底层都是这套"parse → NodeTransformer → fix_missing_locations → compile"的流水线。
十二、注解相关特殊属性:__annotations__ 与 __annotate__
第三节讲了函数注解怎么写,3.15.1 与第十节讲了 3.14+ 的惰性求值和 annotationlib。这一节补两个"属性本身":__annotations__(老字典,一直都能用)和 __annotate__()(3.14+ 新增的惰性求值入口),并把它们和第十节三种格式串起来。
12.1、__annotations__:注解的"老字典"
只要对象上写了注解,Python 就会把它们收集进一个名为 __annotations__ 的 dict:参数名 → 注解值,返回值用特殊键 "return"。函数、类、模块都有(本机 Python 3.13.16 实测):
# ① 函数上:参数 + return 一起进字典
def add(a: int, b: int) -> int:
return a + b
print(add.__annotations__)
# 输出:{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
# ② 类上:类体里写的 `名字: 类型` 全部进 __annotations__
class Point:
x: int
y: float = 0.0
label: str = "pt"
print(Point.__annotations__)
# 输出:{'x': <class 'int'>, 'y': <class 'float'>, 'label': <class 'str'>}
# ③ 模块上:模块级注解同样收进模块自己的 __annotations__
mod_x: int = 10
mod_name: str = "demo"
import sys
print(sys.modules[__name__].__annotations__)
# 输出:{'mod_x': <class 'int'>, 'mod_name': <class 'str'>}
没有写任何注解的函数,__annotations__ 是个空字典 {},不会报错:
def plain(x):
return x
print(plain.__annotations__) # 输出:{}
小嘉提醒:实例
p = Point()上也能p.__annotations__,但那是顺着类查上来的(读到的还是Point.__annotations__),并不是实例自己存了一份。想给某个实例单独挂元数据,别用这个属性,直接在p.__dict__里塞别的字段即可。
12.2、from __future__ import annotations 会把注解变成字符串
文件头一旦写了 from __future__ import annotations(PEP 563),所有注解在定义时不再求值,而是原样塞进字符串。下面两段代码放在两个不同文件里跑,对比一目了然(本机 3.13 实测):
# 文件 A:不带 future
def add(a: int, b: int) -> int: ...
add.__annotations__
# {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
# 文件 B:顶部写了 from __future__ import annotations
def add(a: int, b: int) -> int: ...
add.__annotations__
# {'a': 'int', 'b': 'int', 'return': 'int'}
值从 <class 'int'> 变成了 'int'(字符串)。这就是 3.15.1 节说的"字符串化前向引用"老办法:注解里就算写了一个还没 import 的类名,模块加载也不会因为 NameError 崩——但代价是你拿到的是字符串,要用 typing.get_type_hints() 再求值回去。
12.3、__annotate__():3.14+ 的惰性求值入口(PEP 649)
PEP 649 之后,函数/类对象上多了一个可调用对象 __annotate__(format)。它是 annotationlib 的底层引擎:
- 调用
func.__annotate__(format)就返回对应格式的注解映射; - 你平时读的
func.__annotations__,其内部实现其实就是去调__annotate__(<旧格式>)并拼成老字典——所以一读__annotations__就立刻求值(3.15.1 节那个bad(x: UndefinedName)一读就NameError,就是这个原因)。
本机实测:Python 3.13.16 上
hasattr(func, '__annotate__')为False,函数/类对象上只有__annotations__、没有__annotate__。PEP 649 是 3.14 才落地的新接口,3.13 无法实测,下面示例按官方文档与 PEP 649 编写,供升级到 3.14/3.15 后对照。
3.14+ 环境下大致长这样(官方文档示例的简化):
# 仅在 Python 3.14+ 上运行
import annotationlib
from annotationlib import Format
def ok(name: str, age: int) -> list[str]:
return []
# __annotate__ 是对象上的一个可调用方法,按格式返回字典
# 与 annotationlib.get_annotations(ok, format=Format.VALUE) 等价
print(ok.__annotate__(Format.VALUE))
# {'name': <class 'str'>, 'age': <class 'int'>, 'return': list[str]}
# STRING 格式:只给源码字符串,不求值
print(ok.__annotate__(Format.STRING))
# {'name': 'str', 'age': 'int', 'return': 'list[str]'}
它和第十节 annotationlib.get_annotations(obj, format=...) 的关系是:一层友好封装 vs 底层原语。写应用代码直接用 annotationlib;写框架/工具库、需要精细控制求值时机和命名空间时才直接碰 __annotate__。
小嘉补充:Python 3.15 的 PEP 767 进一步统一了
__annotate__()的格式参数与annotationlib.Format的对应关系(让底层接口和高层 API 用同一套格式枚举),日常使用时直接调annotationlib.get_annotations即可,不必关心这层统一。
小嘉提醒:在 3.13 及更早版本上,
__annotate__不存在,只能读__annotations__(立刻求值)或配合from __future__ import annotations拿字符串;3.14+ 之后,老代码里"一读__annotations__就崩"的前向引用场景,都应改走__annotate__(Format.STRING/FORWARDREF)或annotationlib,不要再直接读老字典。
练习题
- 用
map把列表[1, 2, 3, 4]全部转成字符串(str),再用list()包起来看结果。 - 用
functools.reduce计算[5, 4, 3, 2, 1]的累乘。 - 写一个装饰器
@upper_result,让被装饰函数的返回值自动变成大写字符串(假设函数返回字符串)。 - 给下面函数加函数注解:
def greet(name, age): return ...,要求name: str、age可能是int也可能不传(用int | None)。 - 用
enumerate遍历["苹果", "香蕉", "橙子"],打印"第1个:苹果"这样的输出(注意下标从 1 开始)。 - 思考题:为什么对用户输入的字符串直接
eval()很危险?应该用什么更安全的方式替代? - 写一个函数
def half(x: int) -> float: return x / 2,先用annotationlib的Format.STRING取出它的注解(不求值),再用Format.VALUE取出求值后的真实对象,对比两者输出的差别。 - 用
TypeVar("T", default=int)定义一个泛型类Box(Generic[T]),分别写Box(1)(不传类型参数)和Box[str]("hi")(显式指定),观察默认值回退;再给map(lambda a, b: a - b, [10, 9, 8], [1, 2], strict=True)加try/except,看看会抛什么异常。 - 用
ast.parse解析字符串"total = a + b * c",用ast.walk找出所有Name节点的名字打印出来,再用ast.dump看整棵树的节点结构。 - 用
functools.partial把round改造成"固定保留 2 位小数"的round2,再用它处理[3.14159, 2.71828]。 - 有一堆字典
[{"name": "张三", "age": 30}, {"name": "李四", "age": 25}],分别用operator.itemgetter("age")和 lambda 两种方式按年龄从大到小排序,对比写法,并说出operator版好在哪。
小结
- 高阶函数:把函数当参数。
map变换、filter过滤、reduce累计、sorted(key=...)自定义排序。 - 装饰器 = 接收函数返回新函数的函数,
@deco语法糖等价f = deco(f);用@wraps保住原函数元信息。 - 带参数装饰器要多包一层;多层装饰器"最近先包装、最上先执行"。
- 函数注解只是提示不做强制类型检查;现代写法用
str | None(PEP 604)。 - 常用 BIF:
callable、repr、abs、round、pow、divmod、enumerate、zip、all、any。 eval/exec能执行任意代码,有安全风险,处理字面量优先用ast.literal_eval。ast是标准库的"抽象语法树"工具:ast.parse把源码解析成树、ast.dump打印结构、ast.walk遍历节点。与eval不同,ast只解析不执行,适合做静态分析和安全审查。- Python 3.14/3.15 注解默认惰性求值:直接读
__annotations__会立即求值并对未定义名抛NameError;按需取用用annotationlib.get_annotations(obj, format=Format.VALUE/FORWARDREF/STRING)。类型参数可用 PEP 696 设默认值(TypeVar("T", default=int)),map加strict=True可在多序列长度不一致时立即抛ValueError。 - 标准库函数式搭档:
functools(partial固定参数、lru_cache/cache缓存、cached_property只算一次、total_ordering补齐比较、singledispatch类型分派);operator(add/lt等运算符函数、itemgetter/attrgetter取键取属性、methodcaller调方法,能替代很多 lambda)。 typing是类型提示的工具箱:TypeVar+Generic写泛型类、Protocol(+runtime_checkable) 做结构化接口约定、NewType防基础类型混用、Literal限定取值、TypedDict定型字典形状、ParamSpec给装饰器保签名、TypeAlias/@final/@overload三个小工具;Callable[[..], 返回]、Iterable等形式读老代码要认得。contextlib是写上下文管理器的工具箱:@contextmanager用生成器手写、ExitStack动态叠多个、suppress只吞指定异常、redirect_stdout/stderr临时改输出去向、nullcontext空占位;3.14+ 的annotationlib用Format.VALUE/STRING/FORWARDREF按需取注解,做静态分析优先STRING、不要直接读会立刻求值的__annotations__;ast.literal_eval安全求值字面量,NodeTransformer配合fix_missing_locations可改写代码树。- 注解的运行时入口:
__annotations__是函数/类/模块上的老字典(参数名→注解值,返回值键为"return"),实例上读到的其实是类字典;from __future__ import annotations会把所有注解值字符串化(PEP 563);3.14+ 新增__annotate__(format)(PEP 649)作为惰性求值底层入口,annotationlib.get_annotations是它的友好封装,优先用 STRING/FORWARDREF 而不是直接读会立刻求值的__annotations__。
扩展阅读
- functools 文档:https://docs.python.org/3/library/functools.html
- operator 文档:https://docs.python.org/3/library/operator.html
- itertools 文档:https://docs.python.org/3/library/itertools.html
- 类型提示入门(PEP 484):https://peps.python.org/pep-0484/
- Python 内置函数一览:https://docs.python.org/3/library/functions.html

浙公网安备 33010602011771号