Skip to content

03-函数式编程工具

难度:⭐⭐⭐ 专家 预计时间:45分钟 前置知识:函数基础、装饰器概念、lambda表达式、闭包 引入版本:Python 2.5+ (functools 模块)

functools 模块专门用于高阶函数(即操作函数的函数)。它是编写装饰器、缓存优化和构建复杂函数逻辑的神器,底层由 C 实现,性能优异。


为什么需要 functools?

问题场景:重复计算导致性能瓶颈

python
# ❌ 递归无缓存:指数级复杂度
def fib(n):
    if n < 2: return n
    return fib(n-1) + fib(n-2)  # fib(40) 耗时 20 秒

# ❌ 属性每次访问都重新计算
class DataService:
    @property
    def config(self):
        return load_config_from_db()  # 每次访问都查数据库

# ❌ 装饰器丢失函数元数据
def timer(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@timer
def process():
    """处理数据"""
    pass

print(process.__name__)  # wrapper (丢失了原函数名)
print(process.__doc__)   # None (丢失了文档)

问题:如何避免重复计算、简化函数接口、保留装饰器元数据?


概念铺垫

┌─────────────────────────────────────────────────────────────┐
│          functools 模块核心组件                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 缓存系列                                                │
│  ─────────────────────────────────────────────              │
│  • lru_cache:LRU缓存装饰器,限制大小                        │
│  • cache:无限制缓存(Python 3.9+)                          │
│  • cached_property:属性缓存(Python 3.8+)                  │
│  • 生活类比:备忘录(查过的结果直接抄)                       │
│                                                             │
│  2. 函数变换                                                │
│  ─────────────────────────────────────────────              │
│  • partial:冻结参数,创建偏函数                              │
│  • partialmethod:partial 的方法版本                         │
│  • 生活类比:预填表单(部分字段已填好)                       │
│                                                             │
│  3. 装饰器工具                                              │
│  ─────────────────────────────────────────────              │
│  • wraps:保留函数元数据                                     │
│  • 生活类比:签名代理(保留原作者署名)                       │
│                                                             │
│  4. 类型分发                                                │
│  ─────────────────────────────────────────────              │
│  • singledispatch:基于类型动态分发                          │
│  • singledispatchmethod:方法版本                            │
│  • 生活类比:快递分拣(按包裹类型分流)                       │
│                                                             │
│  5. 归约工具                                                │
│  ─────────────────────────────────────────────              │
│  • reduce:序列累积归约                                      │
│  • 生活类比:折叠账单(逐项累加汇总)                         │
│                                                             │
│  使用决策树:                                                │
│  计算慢?→ lru_cache / cache                                 │
│  属性贵?→ cached_property                                   │
│  参数多?→ partial                                           │
│  写装饰器?→ wraps                                           │
│  类型分发?→ singledispatch                                  │
│  累积逻辑?→ reduce                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

L1 理解层:会用

lru_cache / cache 缓存装饰器

语法结构
┌─────────────────────────────────────────────────────────────┐
│  缓存装饰器语法                                              │
│                                                             │
│  @lru_cache(maxsize=128)    # LRU缓存,限制大小              │
│  @lru_cache(maxsize=None)   # 无限制缓存                     │
│  @cache                     # 无限制缓存简写(3.9+)          │
│                                                             │
│  参数:                                                      │
│  maxsize:缓存容量,None 表示无限                             │
│  typed:是否区分参数类型(False=不区分)                      │
│                                                             │
│  方法:                                                      │
│  .cache_info()              # 获取缓存统计                   │
│  .cache_clear()             # 清空缓存                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
最简示例
python
from functools import lru_cache

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

print(fib(40))  # 102334155(毫秒级)
print(fib.cache_info())  # CacheInfo(hits=..., misses=...)
详细示例
python
from functools import lru_cache, cache
import time

# lru_cache:限制缓存大小
@lru_cache(maxsize=128)
def expensive_compute(n: int) -> int:
    time.sleep(0.1)  # 模拟耗时操作
    return n * n

print(expensive_compute(5))  # 第一次:计算
print(expensive_compute(5))  # 第二次:缓存命中

# cache:无限制缓存(Python 3.9+)
@cache
def factorial(n: int) -> int:
    if n < 2: return 1
    return n * factorial(n - 1)

print(factorial(10))  # 第一次计算
print(factorial(10))  # 缓存命中
print(factorial.cache_info())  # 查看缓存统计

# cache_clear:清空缓存
expensive_compute.cache_clear()
print(expensive_compute(5))  # 再次计算
关键代码说明
代码含义为什么这样写
@lru_cache(maxsize=128)限制缓存128个结果防止内存无限增长
@cache无限制缓存参数组合少时更简洁
.cache_info()查看命中统计监控缓存效率
.cache_clear()清空缓存强制重新计算

cached_property 属性缓存

语法结构
┌─────────────────────────────────────────────────────────────┐
│  cached_property 语法                                        │
│                                                             │
│  @cached_property                                           │
│  def method(self):                                          │
│      ...                                                    │
│                                                             │
│  特点:                                                      │
│  • 首次访问计算,后续直接返回缓存                             │
│  • 线程安全                                                  │
│  • 可用 del 清除缓存                                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
最简示例
python
from functools import cached_property

class Config:
    @cached_property
    def data(self):
        print("加载配置...")
        return {"host": "localhost"}

c = Config()
print(c.data)  # 加载配置...
print(c.data)  # 直接返回缓存
关键代码说明
代码含义为什么这样写
@cached_property属性缓存只计算一次,后续访问零成本
del obj.prop清除缓存强制重新计算

partial 偏函数

语法结构
┌─────────────────────────────────────────────────────────────┐
│  partial 语法                                                │
│                                                             │
│  partial(func, *args, **kwargs)                             │
│                                                             │
│  冻结部分参数,生成新函数                                     │
│                                                             │
│  示例:                                                      │
│  binary_int = partial(int, base=2)                          │
│  binary_int("1010")  # 等价于 int("1010", base=2)           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
最简示例
python
from functools import partial

binary_int = partial(int, base=2)
print(binary_int("1010"))  # 10
print(binary_int("1111"))  # 15
详细示例
python
from functools import partial

# 冻结多个参数
multiply = partial(lambda x, y, z: x * y * z, y=2, z=3)
print(multiply(4))  # 24(4 * 2 * 3)

# 回调函数传参
def on_click(button_id, event):
    print(f"按钮 {button_id} 点击: {event}")

btn1_click = partial(on_click, button_id=1)
btn1_click("click")  # 按钮 1 点击: click

wraps 装饰器工具

语法结构
┌─────────────────────────────────────────────────────────────┐
│  wraps 语法                                                  │
│                                                             │
│  @wraps(func)                                               │
│  def wrapper(*args, **kwargs):                              │
│      ...                                                    │
│                                                             │
│  保留的属性:                                                 │
│  __name__, __doc__, __module__, __annotations__             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
最简示例
python
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@timer
def process():
    """处理数据"""
    pass

print(process.__name__)  # process(保留了)
print(process.__doc__)   # 处理数据(保留了)

singledispatch 类型分发

语法结构
┌─────────────────────────────────────────────────────────────┐
│  singledispatch 语法                                         │
│                                                             │
│  @singledispatch                                            │
│  def func(arg):                                             │
│      ...                                                    │
│                                                             │
│  @func.register                                             │
│  def _(arg: Type):                                          │
│      ...                                                    │
│                                                             │
│  根据第一个参数类型分发                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
最简示例
python
from functools import singledispatch

@singledispatch
def process(data):
    raise NotImplementedError("Unsupported type")

@process.register
def _(data: int):
    return data * 2

@process.register
def _(data: str):
    return data.upper()

print(process(10))    # 20
print(process("hi"))  # HI
详细示例
python
from functools import singledispatch

@singledispatch
def serialize(data):
    """序列化数据"""
    return str(data)

@serialize.register
def _(data: dict):
    import json
    return json.dumps(data)

@serialize.register
def _(data: list):
    return "[" + ", ".join(serialize(x) for x in data) + "]"

@serialize.register
def _(data: int):
    return str(data)

print(serialize({"a": 1}))  # '{"a": 1}'
print(serialize([1, 2, 3])) # '[1, 2, 3]'

reduce 归约累积

语法结构
┌─────────────────────────────────────────────────────────────┐
│  reduce 语法                                                 │
│                                                             │
│  reduce(func, iterable, initializer=None)                   │
│                                                             │
│  func:二元函数 (x, y) → result                              │
│  iterable:可迭代对象                                        │
│  initializer:初始值(可选)                                 │
│                                                             │
│  执行过程:                                                   │
│  reduce(f, [a, b, c]) = f(f(a, b), c)                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
最简示例
python
from functools import reduce

numbers = [1, 2, 3, 4, 5]
result = reduce(lambda x, y: x * y, numbers)
print(result)  # 120(1*2*3*4*5)
详细示例
python
from functools import reduce

# 连乘(阶乘)
nums = [1, 2, 3, 4, 5]
product = reduce(lambda x, y: x * y, nums)
print(product)  # 120

# 合并字典
dicts = [{"a": 1}, {"b": 2}, {"a": 3}]
merged = reduce(lambda d1, d2: {**d1, **d2}, dicts)
print(merged)  # {'a': 3, 'b': 2}

# 带初始值
total = reduce(lambda x, y: x + y, [], 0)
print(total)  # 0(空列表用初始值)

L2 实践层:用好

lru_cache / cache 推荐做法

做法原因示例
参数必须可哈希缓存用字典存储int, str, tuple ✅;list, dict ❌
设置合理的 maxsize防止内存溢出参数组合多时设128或256
纯函数优先使用无副作用,缓存安全数学计算、数据转换
监控 cache_info评估缓存效果命中率低则调整策略

lru_cache / cache 反模式

python
# ❌ 不可哈希参数(报错)
@lru_cache
def process(items: list):  # list 不可哈希
    return sum(items)

process([1, 2, 3])  # TypeError: unhashable type: 'list'

# ✅ 使用元组(可哈希)
@lru_cache
def process(items: tuple):
    return sum(items)

process((1, 2, 3))  # 正常工作
python
# ❌ 有副作用的函数(缓存导致副作用丢失)
@lru_cache
def send_email(user_id: int) -> bool:
    # 缓存后,相同 user_id 只发送一次邮件
    return api.send_mail(user_id)

# ✅ 缓存结果,副作用单独处理
@lru_cache
def get_email_content(user_id: int) -> str:
    return generate_content(user_id)

def send_email(user_id: int) -> bool:
    content = get_email_content(user_id)
    return api.send_mail(user_id, content)

lru_cache / cache 适用场景

场景是否推荐原因
递归算法✅ 推荐消除重复计算
数学计算✅ 推荐纯函数,结果稳定
数据转换✅ 推荐输入相同,输出相同
API调用⚠️ 谨慎数据可能变化
写操作❌ 不推荐有副作用

cached_property 推荐做法

做法原因示例
计算成本高的属性避免重复计算数据库查询、网络请求
只读配置属性一次加载,多次使用config.yaml 解析
线程安全场景自动处理并发多线程访问同一属性

cached_property 反模式

python
# ❌ 可变属性用 cached_property(缓存后无法更新)
class Config:
    @cached_property
    def settings(self):
        return load_settings()
    
    def update_settings(self, new_value):
        self.settings['key'] = new_value  # 修改的是缓存副本

# ✅ 需要可变时用普通 property
class Config:
    @property
    def settings(self):
        if self._settings is None:
            self._settings = load_settings()
        return self._settings
    
    def update_settings(self, new_value):
        self._settings['key'] = new_value

cached_property 适用场景

场景是否推荐原因
数据库连接信息✅ 推荐只读,一次加载
配置解析✅ 推荐解析成本高
计算密集型属性✅ 推荐避免重复计算
需要动态更新❌ 不推荐缓存无法自动刷新

partial 推荐做法

做法原因示例
简化重复调用避免每次传相同参数partial(int, base=2)
回调函数绑定GUI/异步场景常用partial(handler, user_id=1)
配置默认值统一函数行为partial(fetch, timeout=30)

partial 反模式

python
# ❌ 简单场景用 partial(过度设计)
add_one = partial(lambda x, y: x + y, y=1)
print(add_one(5))

# ✅ 直接写函数更清晰
def add_one(x):
    return x + 1

print(add_one(5))

wraps 推荐做法

做法原因示例
所有装饰器必用保留函数元数据@wraps(func)
保持调试友好日志显示原函数名traceback 更清晰

wraps 反模式

python
# ❌ 不使用 wraps(丢失元数据)
def my_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def say_hello():
    """Say hello."""
    pass

print(say_hello.__name__)  # wrapper(错误)
print(say_hello.__doc__)   # None(丢失)

# ✅ 使用 wraps
from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

singledispatch 推荐做法

做法原因示例
类型判断替代 if-else更清晰、可扩展@process.register(int)
第三方类型支持注册外部类型@process.register(numpy.ndarray)
默认处理抛异常明确不支持类型raise NotImplementedError

singledispatch 反模式

python
# ❌ 用 if-else 判断类型(冗长、难扩展)
def process(data):
    if isinstance(data, int):
        return data * 2
    elif isinstance(data, str):
        return data.upper()
    elif isinstance(data, list):
        return [x * 2 for x in data]
    else:
        raise TypeError(f"Unsupported type: {type(data)}")

# ✅ 用 singledispatch(清晰、可扩展)
from functools import singledispatch

@singledispatch
def process(data):
    raise TypeError(f"Unsupported type: {type(data)}")

@process.register
def _(data: int):
    return data * 2

@process.register
def _(data: str):
    return data.upper()

@process.register
def _(data: list):
    return [x * 2 for x in data]

singledispatch 适用场景

场景是否推荐原因
多类型处理✅ 推荐代码清晰、易扩展
序列化/反序列化✅ 推荐类型明确
验证/转换✅ 推荐按类型分发
简单两三种类型❓ 可选if-else 也足够

reduce 推荐做法

做法原因示例
复杂累积逻辑自定义归约规则字典合并、树构建
简单场景用内置更高效易读sum() 代替 reduce 求和

reduce 反模式

python
# ❌ 简单求和用 reduce(冗长)
total = reduce(lambda x, y: x + y, numbers)

# ✅ 使用内置 sum(更简洁高效)
total = sum(numbers)

# ❌ 简单求最大值用 reduce
maximum = reduce(lambda x, y: x if x > y else y, numbers)

# ✅ 使用内置 max
maximum = max(numbers)

reduce 适用场景

场景是否推荐原因
连乘✅ 推荐无内置替代
字典合并✅ 推荐自定义逻辑
树/图构建✅ 推荐复杂累积
求和/最大值❌ 不推荐有更简洁的内置函数

L3 专家层:深入

lru_cache 底层原理

Python 如何实现

lru_cache 使用双向链表 + 字典实现 LRU 淘汰:

python
from functools import lru_cache

@lru_cache(maxsize=3)
def compute(n):
    return n * 2

compute(1)  # cache: {1}
compute(2)  # cache: {1, 2}
compute(3)  # cache: {1, 2, 3}
compute(4)  # cache: {2, 3, 4},淘汰 1(最久未用)
compute(1)  # cache: {3, 4, 1},重新计算,淘汰 2
lru_cache 性能考量
操作时间复杂度说明
缓存命中O(1)字典查找
缓存未命中O(f)f 是原函数复杂度
LRU淘汰O(1)双向链表头部删除

注意:maxsize 远小于参数组合数量时,频繁淘汰会导致缓存命中率下降。

lru_cache 设计动机
设计选择原因
双向链表O(1) 淘汰最久未用项
字典存储O(1) 查找缓存结果
typed=False合并相同值的不同类型(1 和 1.0)
lru_cache 知识关联
缓存知识关联:
┌─────────────────┐     ┌─────────────────┐
│    dict         │────→│   lru_cache     │
│   字典基础      │     │   缓存存储      │
└─────────────────┘     └─────────────────┘


                         ┌─────────────────┐
                         │  双向链表        │
                         │  LRU 淘汰       │
                         └─────────────────┘

cached_property 底层原理

Python 如何实现

cached_property 是描述符实现:

python
from functools import cached_property

class Config:
    @cached_property
    def data(self):
        return load_config()

# 实际执行过程
c = Config()
c.data  # 第一次:调用 __get__,计算并存储到 __dict__
c.data  # 第二次:直接从 __dict__ 读取,不触发描述符
描述符机制
┌─────────────────────────────────────────────────────────────┐
│  cached_property 描述符流程                                  │
│                                                             │
│  第一次访问 obj.prop:                                       │
│  1. 查 obj.__dict__ → 未找到                                │
│  2. 查类描述符 → 找到 cached_property                        │
│  3. 调用 __get__(obj, type)                                 │
│  4. 计算属性值                                               │
│  5. 存入 obj.__dict__['prop']                               │
│  6. 返回结果                                                 │
│                                                             │
│  第二次访问 obj.prop:                                       │
│  1. 查 obj.__dict__ → 找到 'prop'                           │
│  2. 直接返回值(不触发描述符)                                │
│                                                             │
│  del obj.prop 后:                                           │
│  1. 删除 obj.__dict__['prop']                               │
│  2. 下次访问重新触发描述符                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
与 property 的区别
特性propertycached_property
存储位置无(每次计算)obj.dict
触发机制每次调用 get首次后直接读取
线程安全无保证有锁保护
可清除不需要del obj.prop
cached_property 性能考量
操作时间复杂度说明
首次访问O(f)f 是计算函数复杂度
后续访问O(1)直接字典读取
清除缓存O(1)删除字典键
cached_property 设计动机
设计选择原因
存入 dict后续访问零成本
描述符实现统一属性访问接口
线程安全多线程场景常见

singledispatch 底层原理

Python 如何实现

singledispatch 使用字典存储类型到函数的映射:

python
from functools import singledispatch

@singledispatch
def process(data):
    return "default"

# 内部结构(简化)
print(process.registry)  # {object: <default_func>, int: <int_func>, ...}
print(process.dispatch(int))  # 返回 int 类型的处理函数
类型查找机制
┌─────────────────────────────────────────────────────────────┐
│  singledispatch 类型查找流程                                 │
│                                                             │
│  process(data)                                              │
│       │                                                     │
│       ↓                                                     │
│  type(data) → 查 registry 字典                              │
│       │                                                     │
│       ↓                                                     │
│  找到?→ 直接调用注册函数                                    │
│       │                                                     │
│       ↓ (未找到)                                            │
│  查父类(MRO顺序)                                           │
│       │                                                     │
│       ↓                                                     │
│  找到父类处理函数?→ 调用                                    │
│       │                                                     │
│       ↓ (未找到)                                            │
│  调用默认函数                                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
singledispatch 性能考量
操作时间复杂度说明
类型查找O(1)字典查找
父类查找O(n)n 是 MRO 长度(通常很小)
函数调用O(f)f 是处理函数复杂度
singledispatch 设计动机
设计选择原因
只根据第一个参数分发实现简单,符合 Python 习惯
用 MRO 查找父类支持继承关系
注册机制可动态添加新类型

本章小结

┌─────────────────────────────────────────────────────────────┐
│                 functools 核心功能回顾                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  工具             用途                    版本要求            │
│  ───────────────  ──────────────────────  ─────              │
│  lru_cache        LRU缓存,可限制大小        2.5+             │
│  cache            无限制缓存                3.9+             │
│  cached_property  属性缓存                  3.8+             │
│  reduce           序列累积归约              2.5+             │
│  partial          冻结参数,偏函数           2.5+             │
│  wraps            装饰器元数据保留           2.5+             │
│  singledispatch   类型动态分发              3.4+             │
│                                                             │
│  决策指南:                                                   │
│  计算慢 → lru_cache / cache                                  │
│  属性贵 → cached_property                                    │
│  参数多 → partial                                            │
│  写装饰器 → wraps                                            │
│  类型分发 → singledispatch                                   │
│  累积逻辑 → reduce                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

自检清单

回答以下问题,检查你是否掌握了核心概念:

  1. lru_cache 和 cache 有什么区别?
  2. 缓存函数的参数为什么必须可哈希?
  3. cached_property 和 property 有什么区别?
  4. partial 的作用是什么?
  5. 为什么装饰器必须使用 wraps?
  6. singledispatch 如何实现类型分发?
  7. reduce 和 sum 有什么区别?

答案

  1. lru_cache 可限制缓存大小;cache 无限制(Python 3.9+)
  2. 缓存用字典存储,字典键必须是可哈希类型
  3. cached_property 首次计算后缓存;property 每次访问都计算
  4. 冻结部分参数,生成简化的新函数
  5. wraps 保留原函数的 __name____doc__ 等元数据,便于调试
  6. 根据第一个参数的类型,查找注册的处理函数
  7. reduce 是通用归约;sum 是求和专用,更高效

本章术语表

术语定义本章位置
lru_cacheLRU缓存装饰器L1理解层
cache无限制缓存装饰器L1理解层
cached_property属性缓存装饰器L1理解层
partial偏函数,冻结参数L1理解层
wraps装饰器元数据保留工具L1理解层
singledispatch类型动态分发装饰器L1理解层
reduce序列累积归约函数L1理解层
maxsizelru_cache 的缓存容量参数L2实践层
可哈希能作为字典键的类型L2实践层
LRULeast Recently Used 最近最少使用L3专家层

扩展阅读

  • Python 官方文档:functools 模块
  • 《流畅的Python》第7章:函数装饰器和闭包
  • Python 3.8 新特性:cached_property
  • Python 3.9 新特性:cache 装饰器