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,不要再直接读老字典。

练习题

  1. 用 map 把列表 [1, 2, 3, 4] 全部转成字符串(str),再用 list() 包起来看结果。
  2. 用 functools.reduce 计算 [5, 4, 3, 2, 1] 的累乘。
  3. 写一个装饰器 @upper_result,让被装饰函数的返回值自动变成大写字符串(假设函数返回字符串)。
  4. 给下面函数加函数注解:def greet(name, age): return ...,要求 name: str、age 可能是 int 也可能不传(用 int | None)。
  5. 用 enumerate 遍历 ["苹果", "香蕉", "橙子"],打印 "第1个:苹果" 这样的输出(注意下标从 1 开始)。
  6. 思考题:为什么对用户输入的字符串直接 eval() 很危险?应该用什么更安全的方式替代?
  7. 写一个函数 def half(x: int) -> float: return x / 2,先用 annotationlib 的 Format.STRING 取出它的注解(不求值),再用 Format.VALUE 取出求值后的真实对象,对比两者输出的差别。
  8. 用 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,看看会抛什么异常。
  9. 用 ast.parse 解析字符串 "total = a + b * c",用 ast.walk 找出所有 Name 节点的名字打印出来,再用 ast.dump 看整棵树的节点结构。
  10. 用 functools.partial 把 round 改造成"固定保留 2 位小数"的 round2,再用它处理 [3.14159, 2.71828]。
  11. 有一堆字典 [{"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__。

扩展阅读

posted @ 2026-11-10 11:28  小嘉游戏  阅读(5)  评论(0)    收藏  举报