作者: andylin02
学习章节: 第 10 章 使用一等函数实现设计模式
关键词: 一等对象|策略模式|命令模式|高阶函数|减少样板代码|函数即对象|globals()|装饰器注册表


一、本章概述

“符合模式并不表示做得对。”

这本就是《流畅的 Python》第 10 章开篇引用的 Ralph Johnson 的话(《设计模式》一书的作者之一)。这句话恰好点明了本章的核心:我们完全可以照着书上的 UML 类图按部就班地实现某个设计模式,但不代表这种做法在 Python 中就是正确的。

在 C++ 或 Java 中写死了一个模式的整体结构,并不意味着在 Python 中也要照搬全收。正如 Peter Norvig 的研究所示:在支持高阶函数的动态语言中,《设计模式》一书中约 70% 的模式(包括策略模式和命令模式)可能被简化或无需显式实现。

而这一切的起点,正是 Python 把函数当作“一等对象”的这一特性。

阅读本章前,建议温习第 7 章(函数是一等对象)和第 9 章(装饰器和闭包)中对函数作为对象的核心概念。实际上,本章的很多简化,正是站在这些概念的基础之上才得以“水到渠成”。

本章内容大纲

  • 设计模式与语言特性的关系:为什么经典设计模式在某些语言中自动消失
  • 策略模式:从“类满天飞”进化到“函数即策略”
  • 自动发现全局策略
  • 命令模式:用函数和 functools.partial 替代 Command
  • 基于装饰器的注册表机制

二、语言特性重塑设计思维

2.1 经典设计模式的“语言依赖”

1994 年出版的《设计模式:可复用面向对象软件的基础》(俗称“四人帮”GoF 书)收纳了 23 种经典设计模式,源码以 C++ 和 Smalltalk 两种静态类型语言为载体。但这些模式的适用性高度依赖语言的特性。

同样的模式,在 C++ 中可能很巧妙,但在 Python 中往往有更简单、更直接的表现方式。举个例子:Python 的生成器是语言原生功能,这让“迭代器模式”在 Python 中直接变得透明,程序员几乎不需要刻意实现“迭代器模式”。

2.2 函数作为一等对象:模式简化的基础

Peter Norvig 的研究指出,在支持高阶函数和多方法的语言中,对经典设计模式的需求大大降低。原因在于,很多模式的核心功能——算法封装和进行行为参数化——本质上是试图在缺乏一等函数的语言里模拟函数式行为。

换句话说:如果函数本身就是一种可被随意传递和存储的“对象”,那么很多“模式”便变得多余了

2.3 一等对象的定义

让我们先明确“一等对象”的含义:

条件 说明
在运行时创建 对象可以在程序运行中动态生成
能赋值给变量或数据结构中的元素 可以存入列表、字典等容器
能作为参数传给函数 可以传递给其他函数
能作为函数的返回结果 可以从函数中返回

Python 中的函数满足以上全部条件。正因为如此,许多以“类”为核心的设计模式(策略模式、命令模式、模版方法模式等),在 Python 中都能被简化为更轻量的函数形式。

三、策略模式:从“类”到“函数”的蜕变

本章以策略模式(Strategy Pattern)作为核心案例,展示了 Python 如何借助一等函数大幅简化这一经典模式。

3.1 经典面向对象的策略模式实现

业务规则:某电商根据用户积分和订单内容提供三种折扣方案,每个订单只能使用其中一种。

  • 忠诚折扣:用户积分 ≥ 1000,整单享 5% 折扣
  • 批量折扣:同一商品数量 ≥ 20,该商品项享 10% 折扣
  • 大订单折扣:订单中不同商品种类 ≥ 10,整单享 7% 折扣

按照《设计模式》书中的“经典”实现方式,我们需要为每个折扣规则定义一个策略类:

from abc import ABC, abstractmethod
from collections.abc import Sequence
from decimal import Decimal
from typing import NamedTuple

class Customer(NamedTuple):
    name: str
    fidelity: int

class LineItem(NamedTuple):
    product: str
    quantity: int
    price: Decimal
    def total(self) -> Decimal:
        return self.price * self.quantity

class Order(NamedTuple):
    customer: Customer
    cart: Sequence[LineItem]
    promotion: 'Promotion | None' = None

    def total(self) -> Decimal:
        return sum(item.total() for item in self.cart, start=Decimal(0))

    def due(self) -> Decimal:
        discount = self.promotion.discount(self) if self.promotion else Decimal(0)
        return self.total() - discount

    def __repr__(self):
        return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'

class Promotion(ABC):
    @abstractmethod
    def discount(self, order: Order) -> Decimal:
        """返回折扣金额"""

class FidelityPromo(Promotion):
    """为积分>=1000的顾客提供5%折扣"""
    def discount(self, order: Order) -> Decimal:
        if order.customer.fidelity >= 1000:
            return order.total() * Decimal('0.05')
        return Decimal(0)

class BulkItemPromo(Promotion):
    """单个商品数量>=20时提供10%折扣"""
    def discount(self, order: Order) -> Decimal:
        discount = Decimal(0)
        for item in order.cart:
            if item.quantity >= 20:
                discount += item.total() * Decimal('0.1')
        return discount

class LargeOrderPromo(Promotion):
    """不同商品数量>=10时提供7%折扣"""
    def discount(self, order: Order) -> Decimal:
        distinct_items = {item.product for item in order.cart}
        if len(distinct_items) >= 10:
            return order.total() * Decimal('0.07')
        return Decimal(0)

代码的问题

  • 每个策略类只包含一个方法 discount,实质上是函数
  • 每个策略需要被实例化:promotion=FidelityPromo()
  • 策略之间没有共享状态,类完全是多余的包装

3.2 经典面向对象策略模式 UML 类图

下面是对应于上面代码的经典策略模式 UML 类图。

Order

-customer: Customer

-cart: list[LineItem]

-promotion: Promotion

+total()

+due()

+repr()

«abstract»

Promotion

+discount(order) : Decimal

FidelityPromo

+discount(order) : Decimal

BulkItemPromo

+discount(order) : Decimal

LargeOrderPromo

+discount(order) : Decimal

3.3 用函数重构策略模式

既然每个策略类只封装了一个单方法接口,而 Python 的函数本身就是可调用对象,为什么不直接用函数呢?

from collections.abc import Sequence
from dataclasses import dataclass
from decimal import Decimal
from typing import Callable, Optional, NamedTuple

class Customer(NamedTuple):
    name: str
    fidelity: int

class LineItem(NamedTuple):
    product: str
    quantity: int
    price: Decimal
    def total(self) -> Decimal:
        return self.price * self.quantity

@dataclass(frozen=True)
class Order:
    customer: Customer
    cart: Sequence[LineItem]
    promotion: Optional[Callable[['Order'], Decimal]] = None     # 函数类型

    def total(self) -> Decimal:
        return sum(item.total() for item in self.cart, start=Decimal(0))

    def due(self) -> Decimal:
        if self.promotion is None:
            discount = Decimal(0)
        else:
            discount = self.promotion(self)                     # 直接调用函数
        return self.total() - discount

    def __repr__(self):
        return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'

def fidelity_promo(order: Order) -> Decimal:
    if order.customer.fidelity >= 1000:
        return order.total() * Decimal('0.05')
    return Decimal(0)

def bulk_item_promo(order: Order) -> Decimal:
    discount = Decimal(0)
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * Decimal('0.1')
    return discount

def large_order_promo(order: Order) -> Decimal:
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * Decimal('0.07')
    return Decimal(0)

重构后的 Order 类:

  • 不再需要抽象基类 Promo
  • promotion 属性接收的是函数对象,而非类实例
  • due 方法直接调用 self.promotion(self)

3.4 为什么需要 self.promotion(self)

初学者会困惑:在 Order 类中调用 self.promotion(self) 有什么特别之处?执行过程如下:

  1. Order 类中,promotion 是一个实例属性,存储的是函数对象(例如 fidelity_promo
  2. self.promotion 从实例中获取函数对象
  3. (self) 将当前 Order 实例作为参数传递给该函数

关键在于:promotion 是保存在属性里的函数,而不是类方法。Python 的自动 self 绑定机制只对类中的方法起作用,对存储在实例属性中的函数无效,因此必须手动传入 self 作为参数。

3.5 类实现 vs 函数实现对比

# 类实现方式(旧)
order = Order(ann, cart, FidelityPromo())   # ⚠️ 需要实例化策略类

# 函数实现方式(新)
order = Order(ann, cart, fidelity_promo)    # ✅ 函数拿来即用

discount = order.due()

3.6 小结:类策略 vs 函数策略优劣对比

对比维度 类策略 函数策略
代码量 每个策略一个类 + 继承 + 实例化 每个策略一个普通函数
状态维护 用实例属性维护状态(需要时) 通过闭包或 nonlocal 维护状态
共享策略 需要结合享元模式手动实现 函数在模块加载时创建一次,天然共享
内存占用 类实例化有一定开销 函数更轻量
可读性 需要理解继承和多态 直接查看函数逻辑

3.7 函数策略执行流程图

具体折扣计算

订单创建

策略函数集

关联

关联

关联

返回折扣金额

最终价格

进入策略函数

fidelity_promo

bulk_item_promo

large_order_promo

创建 Order 实例

将促销函数赋给 promotion 属性

调用 promotion(self)

访问 order.customer.fidelity

遍历 order.cart

检查 len(distinct_items)

order.due()

四、自动发现策略

4.1 best_promo 的问题

现实电商场景中,有时需要自动选出折扣最大的策略,而不是让调用者手动选择。最容易想到的方式是预定义一个策略函数列表,手动维护某个列表里存所有策略函数。

promos = [fidelity_promo, bulk_item_promo, large_order_promo]

def best_promo(order: Order) -> Decimal:
    return max(promo(order) for promo in promos)

这种方法每次添加新的折扣策略,开发人员不仅要定义函数,还得手动追加到列表中——这个隐患迟早会暴露。

4.2 内省全局命名空间

利用 Python 的 globals() 函数,可以自动扫描当前模块中的策略函数。

# 方案一:扫描当前模块的全局命名空间
promos = [func for name, func in globals().items() 
          if name.endswith('_promo') and name != 'best_promo']

def best_promo(order: Order) -> Decimal:
    return max(promo(order) for promo in promos)

4.3 独立模块中的策略收集

更优雅的做法:将所有策略用一个装饰器自动注册,避免手动维护列表的重复劳动。

# promotions.py - 独立模块
from decimal import Decimal

# 用列表存储折扣策略
def discount(order: Order) -> Decimal:
    return max(promo(order) for promo in _promos)

# 让开发者手动注册的策略列表。这依然需要手动维护。
promos = []

def register_promo(func):
    promos.append(func)
    return func

globals() 扫描模块级 _promo 函数仍然需要对命名约束的硬依赖。更显式的做法:用装饰器自动注册。这会将“将函数增加到全局列表”这一行为,注入到每个策略函数的定义阶段。

# 定义一个注册器
promos = []

def register(func):
    promos.append(func)
    return func

@register
def fidelity_promo(order: Order) -> Decimal:
    ...

@register
def bulk_item_promo(order: Order) -> Decimal:
    ...

装饰器注册的优点:

  • 更显式:一眼就看出 @register 意味着“这个函数要被纳入策略库”
  • 更灵活:注册逻辑可以按需扩展,不用局限于命名约定
  • 更健壮:避免因函数命名不符合 *_promo 约定而错过某些策略

4.4 装饰器注册的技术原理

装饰器注册模式已在 Flask、FastAPI 等框架的插件架构中得到实际验证。

REGISTRY = set()

def register(f):
    REGISTRY.add(f)
    return f

将这个注册器用于策略模式时,每当函数定义时,注册器自动将函数引用存入 REGISTRY 集合,而 best_promo 只需遍历 REGISTRY 即可找到所有策略。

这种模式的优势:对终端用户来说创建新策略只需写一个普通的函数并加上 @register 装饰器,无需触及核心模块。

五、命令模式:用函数替代 Command 类

5.1 传统命令模式结构

命令模式的目标是解耦操作的调用者和具体实现者

在 GUI 编程中,典型的做法:自定义一个 Command 类,类里包含一个 execute 方法。系统中的其他组件只需依赖 Command 接口,而不用关心执行细节。

但 Python 中的函数天然是可调用对象,函数本身就是“现成的 Command”。将命令实现为类只是为了满足某些编程风格的要求,在 Python 中完全可以用函数取代。

5.2 functools.partial 在命令模式中的应用

functools.partial 的作用是“冻结”函数的某些参数,返回一个新的可调用对象。

from functools import partial

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

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

partial 实现命令模式时,Menu 对象直接在菜单项中存储 partial 对象(即预绑定了特定参数的函数),用户点击菜单项时直接调用该函数对象。

from functools import partial

def open_file(filename):
    return f"Opened {filename}"

def close_file(filename):
    return f"Closed {filename}"

def save_file(filename):
    return f"Saved {filename}"

class MenuItem:
    def __init__(self, label, command):
        self.label = label
        self.command = command
    def click(self):
        return self.command()

menu = [
    MenuItem("Open", partial(open_file, "data.txt")),
    MenuItem("Close", partial(close_file, "data.txt")),
    MenuItem("Save", partial(save_file, "data.txt")),
]

for item in menu:
    print(item.click())

命令模式的类结构和函数化简化流程示意图:

传统命令模式
Command 接口 + execute() 方法

每个命令对应一个类
例如 OpenCommand

众多类的重复样板代码

利用一阶函数 + functools.partial 重构后的命令模式:

业务函数
open_file(param)

Partial 应用
partial(open_file, 'data.txt')

直接存储和执行
command()

六、模式抉择与总结

6.1 几种实现方式的选择建议

场景 推荐实现 说明
策略简单且无状态 函数 直接使用普通函数
策略有状态但简单 闭包 外部函数返回内部函数,捕获自由变量
策略有复杂状态且需要多种操作方法 用类封装状态和行为
策略需要运行时注册/加载 装饰器注册表 @register 样式维护和自动发现策略
策略需要独立模块动态扫描 globals() + 命名约定 按命名模式自动收集策略函数
命令模式参数预设 functools.partial 冻结部分参数,生成可调用的新对象

6.2 本章模式简化的核心思想

设计模式作为语言特性的间接“补偿”这一视角,能从根本上摆脱“照搬 UML 画法就等于正确”的僵化思维 [8†L6]。在 Python 中,当我们面临以下场景时:

  • 传统模式涉及大量类层次结构
  • 每个具体策略只实现一个方法
  • 策略没有或很少需要维护内部状态

果断选择函数替代那些无状态的类,最终写出更 Pythonic、更简洁易懂又易于维护的代码。

七、本章思维导图

第10章 使用一等函数实现设计模式

核心基础

一等对象

运行时创建 / 赋值 / 作为参数传递 / 作为返回值

为何可简化:函数是一等对象 → 可参数化、可存储

策略模式精析

经典实现: 继承 + 抽象基类

问题: 每个策略一个类 → 代码冗余

函数版策略

promotion: Callable[[Order],Decimal]

self.promotion(self)

优势: 减少样板代码 / 更轻量 / 天然可共享

自动发现策略

globals() + 命名约定

@register装饰器

命令模式

functools.partial

冻结参数 → 生成新函数
partial(func, *args, **kwargs)

函数取代 Command 类

命令 = 函数对象

章节小结

核心原则: 无状态 → 函数 | 简单状态 → 闭包 | 复杂状态 → 类

核心启示: 设计模式不是教条,它依赖语言特性

八、本章总结

回望本章,它传递的一个重要讯息是:设计模式并不总是普适的,它们的价值与实现策略因语言特性不同而异

  • 策略模式进化:在 Python 中,不含状态的策略,从继承抽象类的做法,简化为直接传递普通函数,代码更简洁且更符合 Python 的惯例。
  • 命令模式简化:用 functools.partial 生成预置了参数的函数对象,省去了繁琐的 Command 类和 execute 方法。
  • 自动发现策略:用 globals() 动态收集策略函数,或者用简单的 @register 装饰器实现更具声明性的插件式架构。这些动态技巧在传统语言中难以优雅实现,但在 Python 中极其实用。

哲学层面:设计模式是语言特性的间接“补偿”——一种语言在语言层面缺乏的资源,才需要用设计与结构来弥补。

对于 Python 来说,一等函数不仅是一种语义特征,它还意味着语言层面的表达力提升,也意味着许多传统设计模式中的类层次可以被大幅简化乃至省去。在后续的学习中,这一理念也会反复出现——无论是理解迭代器与生成器的关系,还是各种创建型模式在 Python 中的简化形态。

九、下一章预告

第 11 章《类型提示进阶》

接下来我们将从“设计模式”的世界切换到“类型系统”的世界。第 11 章将延续第 8 章对类型提示的介绍,带领读者进入类型系统顶层设计、型变和泛型约束等核心概念:

  • typing.TypeVar 的边界:限制泛型的适用类型范围。
  • 协变 (Covariant) 与逆变 (Contravariant):出现在泛型协变、逆变等复杂但很有吸引力的主题中。
  • typing.Protocol 进阶应用:结构化子类型的强大之处。
  • typing.Literal, typing.Final, @overload 等高级类型特性。
  • 静态鸭子类型和类型规则:实现可靠的静态子类型关系而无需显式继承。

在实际开发的复杂 API 和框架设计上,类型提示将会提供护航式的保障和支持。


本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

更多推荐