欢迎光临
我们一直在努力

不止是装饰器:把 Python `functools` 真正用起来的完整指南

在这里插入图片描述

不止是装饰器:把 Python functools 真正用起来的完整指南

很多人第一次接触 functools,往往是因为 @wraps。
再往后一点,学到 partial、lru_cache,就觉得这个模块差不多已经掌握了。
但真正在项目里用久了你会发现:functools 其实不是一个“零散工具箱”,而是一组非常有 Python 味道的函数式编程增强能力。

它不花哨,却很实用。
它不重,但非常容易把代码从“能写”提升到“优雅、可复用、可维护”。

这篇文章,我想系统地把 functools 讲透,包括:

  • functools 到底解决什么问题
  • wraps、partial、lru_cache、reduce、cmp_to_key、cached_property 等核心能力怎么用
  • 哪些场景特别适合上 functools
  • 哪些地方容易被误用
  • 在真实项目中,怎么把这些工具用得自然,而不是“炫技”

如果你之前只把它当作“装饰器辅助模块”,那这篇文章大概率会帮你把认知往前推进一大截。


一、functools 是什么?

functools 是 Python 标准库中的一个模块,名字来自 function tools,也就是“函数工具”。

它的核心目标可以概括成一句话:

让函数变得更容易组合、包装、缓存、比较和复用。

你可以把它理解成 Python 里偏“函数式编程风格”的一组增强工具。

它主要解决几类问题:

问题类型functools 提供的能力
装饰器会丢失原函数元信息 wraps
想预先固定部分参数 partial
某些函数结果可重复利用 lru_cache / cache
想把旧式比较函数用于排序 cmp_to_key
想把一组值折叠成一个结果 reduce
类中某个昂贵属性想只计算一次 cached_property
想支持单分派泛型函数 singledispatch / singledispatchmethod

换句话说,functools 不是为了让你“少写几行代码”,而是为了让代码:

  • 更符合抽象
  • 更适合复用
  • 更利于性能优化
  • 更清晰地表达意图

二、为什么 functools 很值得认真学?

很多人学 Python 时,标准库都是按功能“扫过去”的。
但 functools 这个模块很特别,它对代码风格的影响非常大。

它真正的价值在于这几件事:

1. 让“函数”成为一等可组合对象

比如你可以:

  • 给函数套壳
  • 给函数预填参数
  • 给函数自动缓存结果
  • 按类型动态分发到不同实现

这其实是在提升函数的表达能力。


2. 让一些重复逻辑从业务代码中抽离出来

例如:

  • 权限校验
  • 日志记录
  • 性能计时
  • 请求重试
  • 参数预配置
  • 昂贵结果缓存

这些逻辑本来会散落在业务代码里,但借助 functools,通常能收束得更优雅。


3. 它几乎是写装饰器时的“标配”

你可以不用装饰器写 Python,但一旦你开始写装饰器,就几乎绕不开 functools.wraps。

而一旦你开始做缓存、函数配置、排序适配,它就更常出现了。


三、先从最常见的开始:functools.wraps

如果要说 functools 里最经典、最常用的功能,很多人第一反应就是 wraps。

1. 为什么需要 wraps?

来看一个最常见的装饰器:

import time

def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} executed in {end start:.4f}s")
return result
return wrapper

@timer
def add(a, b):
"""返回两个数的和"""
return a + b

调用当然没问题:

print(add(1, 2))

但你检查一下元信息:

print(add.__name__)
print(add.__doc__)

输出大概率会变成:

wrapper
None

这就是问题所在:
装饰器把原函数“包起来”之后,原函数的名字、文档字符串、模块信息等都丢了。

这会带来很多麻烦:

  • 调试信息不准确
  • 日志里函数名错乱
  • 文档工具识别失败
  • 某些依赖反射的框架行为异常

2. 正确写法:加上 @wraps

import time
from functools import wraps

def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} executed in {end start:.4f}s")
return result
return wrapper

@timer
def add(a, b):
"""返回两个数的和"""
return a + b

现在再看:

print(add.__name__) # add
print(add.__doc__) # 返回两个数的和

这就对了。


3. wraps 本质做了什么?

它会把原函数的一些关键属性复制到包装函数上,比如:

  • __name__
  • __doc__
  • __module__
  • __annotations__

并且还会设置 __wrapped__ 属性,让你能追溯到原函数。

例如:

print(add.__wrapped__)

这在调试和某些框架内部非常有用。


4. 写装饰器时的一个基本原则 ✅

📌 只要你在写装饰器,几乎都应该使用 @wraps。

不是“有时候用”,而是“默认就该用”。

除非你非常明确地想隐藏原函数信息,否则不加 wraps 基本都算是不完整写法。


四、写几个真实可用的装饰器,顺便把 wraps 用熟

1. 计时装饰器

import time
from functools import wraps

def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"[timer] {func.__name__}: {end start:.6f}s")
return result
return wrapper


2. 日志装饰器

from functools import wraps

def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[log] calling {func.__name__}, args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[log] {func.__name__} returned {result}")
return result
return wrapper


3. 权限校验装饰器

from functools import wraps

def require_admin(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_admin"):
raise PermissionError("需要管理员权限")
return func(user, *args, **kwargs)
return wrapper

@require_admin
def delete_user(user, user_id):
return f"用户 {user_id} 已删除"


4. 多个装饰器叠加时为什么更需要 wraps?

@log_call
@timer
def compute():
return sum(range(100000))

如果中间有哪个装饰器没加 wraps,那最外层拿到的函数元信息就会越来越混乱。

所以在团队项目里,wraps 不是“锦上添花”,而是装饰器规范的一部分。


五、partial:把函数“预配置”起来

这是 functools 里另一个非常实用的工具。

1. 它解决什么问题?

有时候你有一个通用函数,但在某个场景里,总是会固定传入某些参数。

例如:

def power(base, exponent):
return base ** exponent

现在你经常要算平方和立方:

power(2, 2)
power(3, 2)
power(4, 3)

你当然可以重新定义两个函数:

def square(x):
return power(x, 2)

def cube(x):
return power(x, 3)

但这类“只是预填了一部分参数”的场景,用 partial 会更自然。


2. 基本用法

from functools import partial

def power(base, exponent):
return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5)) # 25
print(cube(2)) # 8

3. partial 的本质

partial 会返回一个新的可调用对象,它相当于:

把原函数的一部分参数提前绑定好,得到一个“新函数”。

这类用法特别适合:

  • 做函数配置
  • 给回调函数预设上下文
  • 适配第三方 API 接口签名
  • 生成更语义化的函数别名

4. 再看几个很实用的例子

场景一:统一日志前缀

from functools import partial

def log(level, message):
print(f"[{level}] {message}")

info = partial(log, "INFO")
warning = partial(log, "WARNING")
error = partial(log, "ERROR")

info("服务启动成功")
warning("磁盘空间不足")
error("数据库连接失败")

这比反复写字符串参数更整洁。


场景二:给 sorted 的 key 预设参数

假设你有一个函数:

def extract_field(record, field):
return record[field]

你可以这样用:

from functools import partial

records = [
{"name": "Tom", "age": 20},
{"name": "Alice", "age": 18},
{"name": "Bob", "age": 22},
]

sort_by_age = partial(extract_field, field="age")
print(sorted(records, key=sort_by_age))

不过这里需要注意参数位置设计是否合理。
如果是你自己写函数,最好优先把“变化更少的参数”放后面,更适合 partial。


场景三:和第三方库回调接口配合

有些回调只允许接收一个参数,但你希望它内部带着更多上下文,就可以借助 partial。

from functools import partial

def handle_event(event, source):
print(f"事件={event}, 来源={source}")

handler = partial(handle_event, source="system-A")
handler("user_login")


六、partial 和 lambda,该怎么选?

很多时候,这两者看起来都能解决问题。

比如:

square = lambda x: power(x, 2)

和:

square = partial(power, exponent=2)

都能实现类似效果。

它们的区别大致可以这么理解:

对比项partiallambda
语义 明确表达“预绑定参数” 更通用,表达任意逻辑
可读性 在“固定部分参数”场景更清晰 简单时也清晰,复杂时容易混乱
灵活性 适合参数绑定 几乎无限制
调试体验 通常更规整 匿名函数调试信息较弱

📌 经验建议:

  • 只是“固定参数”时,优先考虑 partial
  • 需要自定义逻辑变换时,再用 lambda

不要把 lambda 当成所有小函数场景的默认解法。


七、lru_cache:最容易带来性能提升的功能之一

这大概是 functools 中最“见效快”的能力之一。

1. 它解决什么问题?

如果一个函数:

  • 输入相同
  • 输出固定
  • 没有副作用
  • 计算成本较高

那么它的结果就很适合缓存。

这就是 lru_cache 发挥作用的地方。


2. 最经典例子:斐波那契数列

先看普通递归:

def fib(n):
if n < 2:
return n
return fib(n 1) + fib(n 2)

这个实现会有大量重复计算。

现在加上缓存:

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n 1) + fib(n 2)

print(fib(100))

性能会直接上一个台阶。


3. maxsize 是什么?

lru_cache 中的 LRU 指的是 Least Recently Used,也就是“最近最少使用”淘汰策略。

@lru_cache(maxsize=128)
def func(x):
...

表示:

  • 最多缓存 128 个结果
  • 缓存满了后,优先淘汰最近最少使用的条目

如果写成:

@lru_cache(maxsize=None)

表示无限缓存,不做淘汰。


4. 查看缓存统计信息

from functools import lru_cache

@lru_cache(maxsize=4)
def square(x):
print(f"computing {x}")
return x * x

square(2)
square(2)
square(3)
square(3)

print(square.cache_info())

可能输出:

CacheInfo(hits=2, misses=2, maxsize=4, currsize=2)

这对于调优很有帮助。


5. 清空缓存

square.cache_clear()

这在测试环境、热更新场景、配置重载场景中很常见。


八、lru_cache 适合哪些场景?

✅ 很适合

  • 递归计算
  • 配置文件解析
  • 字典映射查找
  • 重复的数据库字典项加载(谨慎)
  • 纯函数结果缓存
  • 昂贵计算的中间结果缓存

❌ 不适合

  • 有副作用的函数
  • 依赖外部实时状态的函数
  • 返回结果会频繁变化的函数
  • 参数不可哈希的函数
  • 结果很占内存且无淘汰策略的场景

一个很重要的前提:参数必须可哈希

例如这类写法会出问题:

from functools import lru_cache

@lru_cache(maxsize=128)
def process(data):
return sum(data)

process([1, 2, 3]) # list 不可哈希

因为 list 不能作为缓存键。

你可以改成元组:

process((1, 2, 3))


九、cache:更直接的无限缓存

在较新的 Python 版本中,还提供了 functools.cache。

它可以看作:

lru_cache(maxsize=None) 的简化版。

示例

from functools import cache

@cache
def fib(n):
if n < 2:
return n
return fib(n 1) + fib(n 2)

什么时候用 cache?

当你明确知道:

  • 就是想无限缓存
  • 不关心 LRU 淘汰策略
  • 希望代码更直白

那它会比 lru_cache(maxsize=None) 更清晰。


十、缓存不是银弹:lru_cache 的几个常见误区

1. 把所有耗时函数都缓存起来

不是“耗时”就一定适合缓存。
如果函数依赖实时数据,缓存反而会带来错误结果。

例如:

import time
from functools import lru_cache

@lru_cache(maxsize=128)
def get_current_time():
return time.time()

这显然不合理。


2. 缓存副作用函数

例如:

from functools import lru_cache

@lru_cache(maxsize=128)
def send_message(user_id):
print(f"给 {user_id} 发消息")
return True

这会让“相同参数只发一次”,通常不是你想要的行为。


3. 忽略内存占用

缓存本质上是在用内存换时间。
如果缓存结果很大、参数组合又很多,内存增长会非常明显。


4. 误把它当成线程安全状态管理工具

lru_cache 是函数结果缓存,不是业务级共享状态管理器。
不要把它当 Redis、不要把它当数据库,也不要指望它解决分布式一致性问题 😄


十一、reduce:把一串值“折叠”成一个结果

很多人第一次见 reduce,会觉得它“很函数式,很高级”。
但也有很多人学完之后,几乎再也没用过。

其实它有用,但要分场景。

1. 什么是 reduce?

它的核心思想是:

把一个可迭代对象,按照某种规则,逐步合并成单个值。

2. 最简单的例子:求和

from functools import reduce

nums = [1, 2, 3, 4, 5]
result = reduce(lambda x, y: x + y, nums)

print(result) # 15

它的执行过程大概是:

(((1 + 2) + 3) + 4) + 5


3. 带初始值

from functools import reduce

nums = [1, 2, 3]
result = reduce(lambda x, y: x + y, nums, 10)

print(result) # 16

执行相当于:

(((10 + 1) + 2) + 3)


4. 乘积计算

from functools import reduce

nums = [1, 2, 3, 4]
product = reduce(lambda x, y: x * y, nums)

print(product) # 24


十二、什么时候该用 reduce,什么时候不该?

这是个很关键的问题。

适合的情况

  • 逻辑本身就是“不断累积/折叠”
  • 写出来比显式循环更清晰
  • 你在表达一种函数式意图

例如:

from functools import reduce

words = ["Python", " ", "is", " ", "great"]
sentence = reduce(lambda a, b: a + b, words)
print(sentence)


不适合的情况

如果 reduce 让代码更难读,那就不要硬上。

例如求和:

from functools import reduce
total = reduce(lambda x, y: x + y, nums)

通常不如:

total = sum(nums)

更清晰。

一个非常实用的判断标准 ✅

📌 如果有更明确的内建函数,就优先用内建函数。

例如:

  • 求和用 sum
  • 求最大值用 max
  • 求最小值用 min
  • 拼接字符串很多时候可以用 "".join(…)

reduce 更适合那些“没有现成语义函数”的折叠逻辑。


十三、cmp_to_key:让旧式比较函数适配现代排序

这是一个很多人平时不常写,但一遇到复杂排序就会觉得非常顺手的工具。

1. 背景

Python 3 里,sorted() 和 .sort() 主要使用 key= 参数,不再推荐旧式的 cmp= 比较函数。

也就是说,你现在通常会写:

sorted(data, key=lambda x: x["age"])

而不是:

# Python 3 中不直接支持这种 cmp 写法
cmp(a, b)

但有时候你手头已经有一个“比较两个对象”的函数,或者复杂排序逻辑用比较函数写更自然,这时就可以用 cmp_to_key。


2. 示例:按字符串长度排序,长度相同按字典序

from functools import cmp_to_key

def compare(a, b):
if len(a) != len(b):
return len(a) len(b)
if a < b:
return 1
elif a > b:
return 1
return 0

words = ["pear", "apple", "fig", "banana", "kiwi"]
result = sorted(words, key=cmp_to_key(compare))

print(result)


3. 为什么有时候它比 key 更方便?

因为某些复杂规则,本质上是“比较两个元素谁更靠前”,直接写比较函数更自然。

例如:

  • 多级复杂规则
  • 特殊优先级队列
  • 自定义领域对象排序
  • 老代码迁移

4. 但平时仍然应优先考虑 key=

因为 key=:

  • 更直观
  • 性能通常更好
  • 更符合 Python 3 排序风格

📌 经验建议:

  • 简单排序:优先 key=
  • 比较逻辑复杂到 key 很别扭时:再考虑 cmp_to_key

十四、total_ordering:少写比较方法

如果你写自定义类排序,经常要实现这些方法:

  • __lt__
  • __le__
  • __gt__
  • __ge__
  • __eq__
  • __ne__

手写起来会比较啰嗦。

1. 基本用法

from functools import total_ordering

@total_ordering
class Student:
def __init__(self, score):
self.score = score

def __eq__(self, other):
return self.score == other.score

def __lt__(self, other):
return self.score < other.score

只要你提供:

  • __eq__
  • 以及一个大小比较方法(如 __lt__)

@total_ordering 就会帮你补出其他比较方法。


2. 使用效果

a = Student(80)
b = Student(90)

print(a < b) # True
print(a <= b) # True
print(a > b) # False
print(a >= b) # False


3. 适合什么场景?

  • 自定义值对象
  • 领域模型比较
  • 排序规则明确的类

4. 注意点

它是为了减少模板代码,不是为了替代你对比较语义的思考。
比较逻辑本身仍然要设计清楚。


十五、cached_property:只算一次的属性

这个功能在类设计里非常有用,尤其适合“计算昂贵,但对象生命周期内通常不会变化”的属性。

1. 先看没有缓存时的问题

class DataSet:
def __init__(self, numbers):
self.numbers = numbers

@property
def total(self):
print("calculating total…")
return sum(self.numbers)

调用:

data = DataSet([1, 2, 3, 4, 5])

print(data.total)
print(data.total)
print(data.total)

每次都会重新计算。


2. 使用 cached_property

from functools import cached_property

class DataSet:
def __init__(self, numbers):
self.numbers = numbers

@cached_property
def total(self):
print("calculating total…")
return sum(self.numbers)

再调用:

data = DataSet([1, 2, 3, 4, 5])

print(data.total)
print(data.total)
print(data.total)

只有第一次会计算,后面直接复用结果。


3. 它和 property 的区别

对比项propertycached_property
每次访问是否重新计算 否,首次后缓存
适合场景 轻量、实时属性 昂贵、稳定属性
数据变更后的同步 天然实时 需手动处理缓存失效

4. 很适合的场景

  • 大文本解析结果
  • 大型列表/字典汇总结果
  • 配置文件加载后的派生属性
  • ORM/数据模型中的昂贵派生字段
  • 复杂正则预处理
  • 图结构、依赖关系分析后的统计结果

5. 使用 cached_property 时的一个重要提醒 ⚠️

如果底层数据会变,那缓存可能过期。

data = DataSet([1, 2, 3])
print(data.total) # 6

data.numbers.append(4)
print(data.total) # 仍然可能是旧值 6

所以它更适合:

  • 初始化后基本不变的数据
  • 或者你可以控制失效逻辑的数据

如果要手动删除缓存:

del data.total
print(data.total) # 会重新计算


十六、singledispatch:按参数类型分发实现

这是 functools 里一个很 Pythonic、但很多人没认真用过的功能。

1. 它解决什么问题?

有时候你想让一个函数:

  • 输入 int 时做一套逻辑
  • 输入 str 时做另一套逻辑
  • 输入 list 时再做另一套逻辑

最普通的写法可能是:

def format_value(value):
if isinstance(value, int):
...
elif isinstance(value, str):
...
elif isinstance(value, list):
...

这当然能写,但随着类型变多,逻辑会不断膨胀。

singledispatch 可以把它拆开。


2. 基本示例

from functools import singledispatch

@singledispatch
def format_value(value):
return f"默认处理: {value}"

@format_value.register
def _(value: int):
return f"整数: {value}"

@format_value.register
def _(value: str):
return f"字符串: {value}"

@format_value.register
def _(value: list):
return f"列表,共 {len(value)} 个元素"

调用:

print(format_value(10))
print(format_value("hello"))
print(format_value([1, 2, 3]))
print(format_value({"a": 1}))

输出:

整数: 10
字符串: hello
列表,共 3 个元素
默认处理: {'a': 1}


3. 它的好处是什么?

  • 每种类型逻辑拆得更清晰
  • 扩展新类型时不改原函数主干
  • 很适合插件式扩展
  • 比冗长的 if isinstance(…) 更规整

4. 适合什么场景?

  • 数据格式化
  • 序列化/反序列化
  • 命令分派
  • 输入类型适配
  • 统一接口,多类型实现

十七、singledispatchmethod:类方法版本的单分派

对于类中的方法,也有对应能力:

from functools import singledispatchmethod

class Printer:
@singledispatchmethod
def print_value(self, value):
print(f"默认输出: {value}")

@print_value.register
def _(self, value: int):
print(f"整数输出: {value}")

@print_value.register
def _(self, value: str):
print(f"字符串输出: {value}")

使用:

p = Printer()
p.print_value(123)
p.print_value("hello")
p.print_value([1, 2, 3])


十八、再聊回装饰器:如何写“带参数的装饰器”?

这是很多人写装饰器时卡住的一步。
而 functools.wraps 在这里同样必不可少。

示例:限制重试次数的装饰器

from functools import wraps

def retry(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exc = None
for _ in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
last_exc = e
raise last_exc
return wrapper
return decorator

使用:

@retry(3)
def unstable():
...

这个结构要记住

带参数装饰器通常是三层:

  • 最外层接收装饰器参数
  • 中间层接收原函数
  • 最内层包装执行逻辑
  • 这是 Python 装饰器里非常经典的写法。


    十九、functools 在真实项目中的几个高频落点

    到这里我们把核心 API 基本都讲完了。
    接下来更重要的问题是:

    这些东西到底应该放在什么地方用?

    下面是一些很典型的项目落点。


    1. 接口层:日志、鉴权、重试、缓存

    • wraps:装饰器规范
    • lru_cache:配置接口结果缓存
    • partial:预配置请求函数
    • cached_property:延迟计算客户端资源

    2. 数据处理层:格式化、映射、排序

    • cmp_to_key:复杂排序规则
    • reduce:聚合逻辑
    • singledispatch:按数据类型分派处理

    3. 领域模型层:对象比较与派生属性

    • total_ordering:值对象排序
    • cached_property:昂贵派生值

    4. 工具函数层:更高复用的函数构造

    • partial:固定上下文
    • wraps:构建标准装饰器库

    二十、一个稍微完整一点的实战例子

    假设你在写一个数据分析工具,需要:

    • 读取配置
    • 缓存配置结果
    • 给核心函数做计时
    • 支持不同输入类型格式化输出

    你可以这样组织:

    import json
    import time
    from functools import wraps, lru_cache, singledispatch

    def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
    start = time.perf_counter()
    result = func(*args, **kwargs)
    end = time.perf_counter()
    print(f"{func.__name__} cost: {end start:.6f}s")
    return result
    return wrapper

    @lru_cache(maxsize=8)
    def load_config(path):
    print(f"loading config from {path}")
    with open(path, "r", encoding="utf-8") as f:
    return json.load(f)

    @singledispatch
    def format_output(value):
    return str(value)

    @format_output.register
    def _(value: dict):
    return json.dumps(value, ensure_ascii=False, indent=2)

    @format_output.register
    def _(value: list):
    return "\\n".join(map(str, value))

    @timer
    def analyze(data):
    return {
    "count": len(data),
    "sum": sum(data),
    "avg": sum(data) / len(data) if data else 0
    }

    你会发现这些工具拼在一起时,代码非常自然。

    这就是 functools 的魅力:
    它不是大而重的框架,而是一些非常轻巧的“结构增强件”。


    二十一、functools 常见误区总结

    1. 写装饰器不用 wraps

    这是最常见、也最不应该的错误之一。


    2. 把 lru_cache 当万能性能优化器

    缓存要建立在“可缓存”的前提上,不是所有函数都适合缓存。


    3. 用 reduce 炫技

    如果 reduce 让读代码的人停下来想 10 秒,那通常就不值得。


    4. partial 滥用到看不出原函数签名

    适度很好,过度会影响理解成本。


    5. cached_property 用在会频繁变化的数据上

    这会带来缓存失效问题。


    6. singledispatch 处理本该属于对象方法多态的逻辑

    有些场景更适合类继承与方法重写,不要为了用而用。


    二十二、如何判断该不该用 functools?

    一个很实用的原则是:

    当你发现下面这些重复结构时,可以考虑它:

    • “我只是想给函数包一层统一逻辑”
    • “这个函数总有几个参数是固定的”
    • “这个结果重复算很多次”
    • “这个属性很贵,但大多数时候不会变”
    • “我想根据类型选择不同处理逻辑”
    • “这个类的比较方法写得太啰嗦了”

    如果你能从这些问题出发,functools 基本都会显得顺手,而不是生硬。


    二十三、学习顺序建议:怎么学最不容易乱?

    我很建议按下面这个顺序掌握。

    第一阶段:先掌握最常用的三件套

  • wraps
  • partial
  • lru_cache
  • 只要这三样吃透,已经能覆盖你日常开发中的大量场景。


    第二阶段:补上类设计和缓存意识

  • cached_property
  • total_ordering
  • 这会让你在写类和对象模型时更舒服。


    第三阶段:理解函数式思维扩展

  • reduce
  • cmp_to_key
  • singledispatch
  • singledispatchmethod
  • 这部分更像“扩展技能包”,不是每天都写,但一旦遇到合适场景会很有价值。


    二十四、给出一份高频 API 速查表

    API作用高频程度
    wraps 保留被装饰函数元信息 ⭐⭐⭐⭐⭐
    partial 预绑定部分参数 ⭐⭐⭐⭐
    lru_cache 基于 LRU 的函数结果缓存 ⭐⭐⭐⭐⭐
    cache 无限缓存 ⭐⭐⭐
    reduce 折叠可迭代对象为单值 ⭐⭐⭐
    cmp_to_key 把比较函数转为排序 key ⭐⭐
    total_ordering 自动补全比较方法 ⭐⭐⭐
    cached_property 只计算一次的属性 ⭐⭐⭐⭐
    singledispatch 基于首参数类型分发函数实现 ⭐⭐⭐
    singledispatchmethod 类方法版本的单分派 ⭐⭐

    二十五、一套很实用的代码风格建议

    ✅ 建议遵循

    • 写装饰器默认加 @wraps
    • 固定参数时优先考虑 partial
    • 缓存前先确认函数是否“纯”
    • cached_property 只用在确实稳定的属性上
    • 简单排序优先 key=,复杂比较再考虑 cmp_to_key
    • reduce 只有在表达更清晰时才用
    • singledispatch 用于“类型分派”,不要代替所有条件判断

    ❌ 尽量避免

    • 为了显得“高级”而堆 functools
    • 在不需要抽象的地方过度封装
    • 用缓存掩盖性能瓶颈的根因
    • 让 partial 和装饰器叠到签名难以理解

    二十六、结尾:functools 的魅力,在于它很“克制”

    Python 里有一些模块,学的时候会让人觉得“哇,很强”。
    而 functools 不太是这种类型。

    它更像一种长期使用后才会慢慢体会到的能力:

    • 它不抢戏
    • 它不庞大
    • 它也不要求你写成纯函数式风格

    但它总能在很合适的地方,帮你把代码拧得更顺、更整洁、更有抽象层次。

    如果要我用一句话概括 functools:

    它不是为了把代码写得更花,而是为了把函数和对象的边界处理得更优雅。

    当你开始自然地使用这些工具时,你会明显感觉到:

    • 装饰器不再粗糙
    • 缓存不再零散
    • 参数配置更清晰
    • 多类型处理更规整
    • 类的设计更干净

    这就是 functools 真正“好用”的地方。✨


    附录:一段非常适合收藏的示例代码

    下面这段代码把几个最常用的能力串在了一起:

    import time
    from functools import wraps, partial, lru_cache, cached_property

    def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
    start = time.perf_counter()
    result = func(*args, **kwargs)
    end = time.perf_counter()
    print(f"{func.__name__} took {end start:.6f}s")
    return result
    return wrapper

    def multiply(a, b):
    return a * b

    double = partial(multiply, 2)
    triple = partial(multiply, 3)

    @lru_cache(maxsize=128)
    def fib(n):
    if n < 2:
    return n
    return fib(n 1) + fib(n 2)

    class Report:
    def __init__(self, values):
    self.values = values

    @cached_property
    def summary(self):
    time.sleep(1) # 模拟昂贵计算
    return {
    "count": len(self.values),
    "sum": sum(self.values),
    "avg": sum(self.values) / len(self.values) if self.values else 0
    }

    @timer
    def run():
    print(double(10))
    print(triple(10))
    print(fib(30))

    report = Report([10, 20, 30, 40])
    print(report.summary)
    print(report.summary) # 第二次直接走缓存

    if __name__ == "__main__":
    run()

    这段代码不复杂,但已经把 functools 最常见、最实用的几种能力体现出来了。


    最后,送你一个非常实用的记忆方式

    你可以把 functools 先记成这五类能力:

    1. 包装函数

    • wraps

    2. 预配置函数

    • partial

    3. 缓存函数/属性

    • lru_cache
    • cache
    • cached_property

    4. 排序与比较增强

    • cmp_to_key
    • total_ordering

    5. 分派与折叠

    • singledispatch
    • singledispatchmethod
    • reduce

    用这个框架去理解,你会比“背 API 名字”轻松很多。


    希望这篇文章不是让你多记几个标准库函数,
    而是让你真正理解:functools 是怎么把 Python 代码变得更稳、更顺、更优雅的。

    赞(0)
    未经允许不得转载:171主机测评 » 不止是装饰器:把 Python `functools` 真正用起来的完整指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址