社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

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

VPointer • 5 年前 • 428 次点击  

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

《流畅的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 ~


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/FKpQPA1Kob
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/13866
 
428 次点击