《流畅的Python》笔记。本篇主要讲述Python中使用函数来实现策略模式和命令模式,最后总结出这种作法背后的思想。python
策略模式若是用面向对象的思想来简单解释的话,其实就是“多态”。父类指向子类,根据子类对同一方法的不一样重写,获得不一样结果。算法
下图是经典的策略模式的UML类图:编程
《设计模式:可复用面向对象软件的基础》一书这样描述策略模式:设计模式
定义一系列算法,把它们封装起来,且使它们能相互替换。本模式使得算法可独立于使用它的客户而变化。bash
下面以一个电商打折的例子来讲明策略模式,打折方案以下:微信
为此咱们须要建立5个类:app
Order
类:订单类,至关于上述UML图中的Context
上下文;Promotion
类:折扣类的父类,至关于UML图中的Strategy
策略类,实现不一样策略的共同接口;FidelityPromo
,BulkPromo
和LargeOrderPromo
依次对应于上述三个打折方案。如下是经典的策略模式在Python中的实现:ide
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())
如今用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中则能够用函数来避开策略模式的这些弊端:
上述代码中,咱们须要自行传入打折策略,但咱们更但愿的是程序自动选择最佳打折策略。如下是咱们最能想到的一种方式:
# 在生成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 --
其实,动态收集具体策略函数更为显式的一种方案是使用简单的装饰器,这将在下一篇中介绍。
命令模式的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()
看完这两个例子,不知道你们发现了什么类似之处了没有:
它们都把实现单方法接口的类的实例替换成了可调用对象。毕竟,每一个Python可调用对象都实现了单方法接口,即__call__
方法。
直白一点说就是,若是你定义了一个抽象类,这个类只有一个抽象方法a()
,而后还要为这个抽象类派生出一大堆具体类来重写这个方法a()
,那么此时大可没必要定义这个抽象类,直接将这些具体类改写成可调用对象便可,在__call__
方法中实现a()
要实现的功能。
这至关于用Python中可调用对象的基类充当了咱们定义的基类,咱们便不用再定义基类;对抽象方法a()
的重写变成了对特殊方法__call__
的重写,毕竟咱们只是想要这些方法有一个相同的名字,至于叫什么其实无所谓。
迎你们关注个人微信公众号"代码港" & 我的网站 www.vpointer.net ~