资讯专栏INFORMATION COLUMN

Python学习之路25-使用一等函数实现设计模式

econi / 1387人阅读

摘要:本篇主要讲述中使用函数来实现策略模式和命令模式,最后总结出这种做法背后的思想。

《流畅的Python》笔记。

本篇主要讲述Python中使用函数来实现策略模式和命令模式,最后总结出这种做法背后的思想。

1. 重构策略模式

策略模式如果用面向对象的思想来简单解释的话,其实就是“多态”。父类指向子类,根据子类对同一方法的不同重写,得到不同结果。

1.1 经典的策略模式

下图是经典的策略模式的UML类图:

《设计模式:可复用面向对象软件的基础》一书这样描述策略模式:

定义一系列算法,把它们封装起来,且使它们能相互替换。本模式使得算法可独立于使用它的客户而变化。

下面以一个电商打折的例子来说明策略模式,打折方案如下:

有1000及以上积分的顾客,每个订单享5%优惠;

同一订单中,每类商品的数量达到20个及以上时,该类商品享10%优惠;

订单中的不同商品达10个及以上时,整个订单享7%优惠。

为此我们需要创建5个类:

Order类:订单类,相当于上述UML图中的Context上下文;

Promotion类:折扣类的父类,相当于UML图中的Strategy策略类,实现不同策略的共同接口;

具体策略类:FidelityPromoBulkPromoLargeOrderPromo依次对应于上述三个打折方案。

以下是经典的策略模式在Python中的实现:

from abc import ABC, abstractmethod
from collections import namedtuple

Customer = namedtuple("Customer", "name fidelity")

class LineItem:  # 单个商品
    def __init__(self, product, quantity, price):
        self.produce = product
        self.quantity = quantity
        self.price = price

    def total(self):
        return self.price * self.quantity

class Order:  # 订单类,上下文
    def __init__(self, customer, cart, promotion=None):
        self.customer = customer
        self.cart = list(cart)  # 形参cart中的元素是LineItem
        self.promotion = promotion

    def total(self):  # 未打折时的总价
        if not hasattr(self, "__total"):
            self.__total = sum(item.total() for item in self.cart)
        return self.__total

    def due(self):  # 折扣
        if self.promotion is None:
            discount = 0
        else:
            discount = self.promotion.discount(self)
        return self.total() - discount

class Promotion(ABC): # 策略:抽象基类
    @abstractmethod  # 抽象方法
    def discount(self, order):
        """返回折扣金额(正值)"""

class FidelityPromo(Promotion): # 第一个具体策略
    """积分1000及以上的顾客享5%"""
    def discount(self, order):
        return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0

class BulkItemPromo(Promotion): # 第二个具体策略
    """某类商品为20个及以上时,该类商品享10%优惠"""
    def discount(self, order):
        discount = 0
        for item in order.cart:
            if item.quantity >= 20:
                discount += item.total() * 0.1
        return discount

class LargeOrderPromo(Promotion): # 第三个具体策略
    """订单中的不同商品达到10个及以上时享7%优惠"""
    def discount(self, order):
        distinct_items = {item.product for item in order.cart}
        if len(distinct_items) >= 10:
            return order.total() * 0.07
        return 0

该类的使用示例如下:

>>> ann = Customer("Ann Smith", 1100)
>>> joe = Customer("John Joe", 0)
>>> cart = [LineItem("banana", 4, 0.5), LineItem("apple", 10, 1.5), 
...         LineItem("watermellon", 5, 5.0)]
>>> Order(ann, cart, FidelityPromo())  # 每次新建一个具体策略类
>>> Order(joe, cart, FidelityPromo())
1.2 Python函数重构策略模式

现在用Python函数以更少的代码来重构上述的策略模式,去掉了抽象类Promotion,用函数代替具体的策略类:

# 不用导入abc模块,去掉了Promotion抽象类;
# Customer, LineItem不变,Order类只修改due()函数;三个具体策略类改为函数
-- snip -- 
class Order:
    -- snip --
    def due(self):  # 折扣
        if self.promotion is None:
            discount = 0
        else:
            discount = self.promotion(self)  # 修改为函数
        return self.total() - discount

def fidelity_promo(order):
    """积分1000及以上的顾客享5%"""
    return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0

def bulk_item_promo(order):
    """某类商品为20个及以上时,该类商品享10%优惠"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * 0.1
    return discount

def large_order_promo(order):
    """订单中的不同商品达到10个及以上时享7%优惠"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * 0.07
    return 0

该类现在的使用示例如下:

>>> Order(ann, cart, fidelity_promo)  # 没有实例化新的促销对象,函数拿来即用

脱离Python语言环境,从面相对象编程来说:

1.1中的使用示例可以看出,每次创建Order类时,都创建了一个具体策略类,即使不同的订单都用的同一个策略。按理说它们应该共享同一个具体策略的实例,但实际并没有。这就是策略模式的一个弊端。为了弥补这个弊端,如果具体的策略没有维护内部状态,你可以为每个具体策略创建一个实例,然后每次都传入这个实例,这就是单例模式;但如果要维护内状态,就需要将策略模式和享元模式结合使用,这又提高了代码行数和维护成本。

在Python中则可以用函数来避开策略模式的这些弊端:

不用维护内部状态时,我们可以直接用一般的函数;如果需要维护内部状态,可以编写装饰器(装饰器也是函数);

相对于编写一个抽象类,再实现这个抽象类的接口来说,直接编写函数更方便;

函数比用户定义的类的实例更轻量;

无需去实现享元模式,每个函数在Python编译模块时只会创建一次,函数本身就是可共享的对象。

1.3 自动选择最佳策略

上述代码中,我们需要自行传入打折策略,但我们更希望的是程序自动选择最佳打折策略。以下是我们最能想到的一种方式:

# 在生成Order实例时,传入一个best_promo函数,让其自动选择最佳策略
promos = [fidelity_promo, bulk_item_promo, large_order_promo] # 三个打折函数的列表
def best_promo(order):
    """选择可用的最佳策略"""
    return max(promo(order) for promo in promos)

但这样做有一个弊端:如果要新增打折策略,不光要编写打折函数,还得把函数手动加入到promos列表中。我们希望程序自动识别这些具体策略。改变代码如下:

promos = [globals()[name] for name in globals() 
          if name.endswith("_promo") and 
          name != "best_promo"] # 自动获取当前模块中的打折函数
def best_promo(order):
    -- snip --

在Python中,模块也是一等对象globals()函数是标准库提供的处理模块的函数,它返回一个字典,表示当前全局符号表。这个符号表始终针对当前模块(对函数或方法来说,是指定义它们的模块,而不是调用它们的模块)

如果我们把各种具体策略多带带放到一个模块中,比如放到promotions模块中,上述代码还可改为如下形式:

# 各具体策略多带带放到一个模块中
import promotions, inspect
# inspect.getmembers函数用于获取对象的属性,第二个参数是可选的判断条件
promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order):
    -- snip --

其实,动态收集具体策略函数更为显式的一种方案是使用简单的装饰器,这将在下一篇中介绍。

2. 命令模式

命令模式的UML类图如下:

命令模式的目的是解耦发起调用的对象(调用者,Caller)和提供实现的对象(接受者,Receiver)。实际做法就是在它们之间增加一个命令类(Command),它只有一个抽象接口execute(),具体命令类实现这个接口即可。这样调用者就无需了解接受者的接口,不同的接受者还可以适应不同的Command子类。

有人说“命令模式是回调机制的面向对象替代品”,但问题是,Python中我们不一定需要这个替代品。具体说来,我们可以不为调用者提供一个Command实例,而是给它一个函数。此时,调用者不用调用command.execute(),而是直接command()

以下是一般的命令模式代码:

from abc import ABC, abstractmethod

class Caller:
    def __init__(self, command=None):
        self.command = command

    def action(self):
        """把对接受者的调用交给中介Command"""
        self.command.execute()

class Receiver:
    def do_something(self):
        """具体的执行命令"""
        print("I"m a receiver")

class Command(ABC):
    @abstractmethod
    def execute(self):
        """调用具体的接受者方法"""

class ConcreteCommand(Command):
    def __init__(self, receiver):
        self.receiver = receiver

    def execute(self):
        self.receiver.do_something()

if __name__ == "__main__":
    receiver = Receiver()
    command = ConcreteCommand(receiver)
    caller = Caller(command)
    caller.action()

# 结果:
I"m a receiver

直接将上述代码改成函数的形式,其实并不容易改写,因为具体的命令类还保存了接收者。但是换个思路,将其改成可调用对象,那么代码就可以变成如下形式:

class Caller:
    def __init__(self, command=None):
        self.command = command

    def action(self):
        # 之前是self.command.execute()
        self.command()

class Receiver:
    def do_something(self):
        """具体的执行命令"""
        print("I"m a receiver")

class ConcreteCommand:
    def __init__(self, receiver):
        self.receiver = receiver

    def __call__(self):
        self.receiver.do_something()

if __name__ == "__main__":
    receiver = Receiver()
    command = ConcreteCommand(receiver)
    caller = Caller(command)
    caller.action()
3. 总结

看完这两个例子,不知道大家发现了什么相似之处了没有:

它们都把实现单方法接口的类的实例替换成了可调用对象。毕竟,每个Python可调用对象都实现了单方法接口,即__call__方法。

直白一点说就是,如果你定义了一个抽象类,这个类只有一个抽象方法a(),然后还要为这个抽象类派生出一大堆具体类来重写这个方法a(),那么此时大可不必定义这个抽象类,直接将这些具体类改写成可调用对象即可,在__call__方法中实现a()要实现的功能。

这相当于用Python中可调用对象的基类充当了我们定义的基类,我们便不用再定义基类;对抽象方法a()的重写变成了对特殊方法__call__的重写,毕竟我们只是想要这些方法有一个相同的名字,至于叫什么其实无所谓。


迎大家关注我的微信公众号"代码港" & 个人网站 www.vpointer.net ~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/44727.html

相关文章

  • Python学习之路24-一等函数

    摘要:函数内省的内容到此结束。函数式编程并不是一个函数式编程语言,但通过和等包的支持,也可以写出函数式风格的代码。 《流畅的Python》笔记。本篇主要讲述Python中函数的进阶内容。包括函数和对象的关系,函数内省,Python中的函数式编程。 1. 前言 本片首先介绍函数和对象的关系;随后介绍函数和可调用对象的关系,以及函数内省。函数内省这部分会涉及很多与IDE和框架相关的东西,如果平时...

    wind3110991 评论0 收藏0
  • 如何合理的规划前端之路

    摘要:什么是前端工程师总而言之就是运用等技术,在工作中配合设计师实现用户界面,和后端工程师进行数据对接,完成应用开发的职位。 什么是前端工程师?总而言之,就是运用 HTML、CSS、JavaScript 等 Web 技术,在工作中配合UI设计师实现用户界面,和后端工程师进行数据对接,完成 Web 应用开发的职位。Tips:个人博客排版、UI更佳;地址:https://haonancx.git...

    skinner 评论0 收藏0
  • 如何合理的规划前端之路

    摘要:什么是前端工程师总而言之就是运用等技术,在工作中配合设计师实现用户界面,和后端工程师进行数据对接,完成应用开发的职位。 什么是前端工程师?总而言之,就是运用 HTML、CSS、JavaScript 等 Web 技术,在工作中配合UI设计师实现用户界面,和后端工程师进行数据对接,完成 Web 应用开发的职位。Tips:个人博客排版、UI更佳;地址:https://haonancx.git...

    lwx12525 评论0 收藏0
  • 如何合理的规划前端之路

    摘要:什么是前端工程师总而言之就是运用等技术,在工作中配合设计师实现用户界面,和后端工程师进行数据对接,完成应用开发的职位。 什么是前端工程师?总而言之,就是运用 HTML、CSS、JavaScript 等 Web 技术,在工作中配合UI设计师实现用户界面,和后端工程师进行数据对接,完成 Web 应用开发的职位。Tips:个人博客排版、UI更佳;地址:https://haonancx.git...

    Pines_Cheng 评论0 收藏0
  • python学习笔记 函数

    摘要:一等函数在中,函数是一等对象。匿名函数关键字在表达式内创建匿名函数然而,简单的句法限制了函数的定义体只能使用纯表达式,即函数的定义体中不能赋值,不能使用等语句。匿名函数适合用于作为函数的参数 一等函数 在python中,函数是一等对象。编程语言理论家把一等对象定义为满足以下条件的程序实体: 在运行时创建 能赋值给变量或数据结构中的元素 能作为参数传给函数 能作为函数的返回结果 在p...

    Scorpion 评论0 收藏0

发表评论

0条评论

econi

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<