本章就Python中函数与设计模式做一些探讨, notebook文件在这里.
一等函数与设计模式
虽然设计模式与语言无关,但是某些设计模式并不适用于某些语言. 下面就Python中策略模式做一些讨论.
策略模式
在设计模式中,策略模式的概述如下:
定义一系列算法,把它们一一封装起来,并且使它们可以互相替换.本模式使得算法可以独立于使用它的客户而变化.
这里举的例子是电商的促销策略,即根据客户的属性或订单中的商品计算折扣(顺便吐槽近几年双十一的折扣是越来越复杂了…).
例如某个网店制定了如下的折扣策略:
- 1000积分以上客户,享受5%折扣
- 同一订单下,单个商品数量达到20个或以上,享受10%折扣
- 订单的不同产品达到10个或以上,享受7%折扣
那么按照策略模式,我们需要有如下的类:
上下文 把一些计算委托给实现的不同算法的可互换组件,它提供服务. 在该例子中,上下文即为订单Order,它会根据不同算法计算促销折扣.
策略 实现不同算法的组建的共同接口. 在该例子中,即为Promotion.
具体策略 策略的具体子类. 在该例子中,即为上述的三种折扣策略.
具体策略由上下文类的客户选择, 在此例子中,我们可以在实例化订单类之前(__init__
)就以某种方式选择好策略, 然后将其作为Order类初始化的参数.
经典策略模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| from abc import ABC, abstractmethod from collections import namedtuple
Customer = namedtuple('Customer', 'name fidelity')
class LineItem: def __init__(self, product, quantity, price): self.product = product self.quantity = quantity self.price = price def total(self): return self.price * quantity
class Order: def __init__(self, customer, cart, promotion=None): self.customer = customer self.cart = cart self.promotion = promotion
class Promotion(ABC): @abstractmethod def discount(self, order): """返回折扣金额""" def FidelityPromo(Promotion): def discount(self, order): return order.total() * 0.5 if order.customer.fiderlity >= 1000 else 0
|
在上面的代码中,给出了顾客的具名元组, 商品类和订单类的例子. 并且这里我们Promotion类是一个抽象基类(ABC), 并且用@abstractmethod
装饰器来装饰抽象方法,没有具体实现该抽象方法的类无法被实例化.
虽然上面的实现没有问题,但是利用函数作为对象,我们可以更加精简地完成这一需求.
利用函数实现策略模式
回顾上面的实现,可以发现貌似我们的具体策略只有一个方法,似乎没有必要将其写成一个类.因此我们可以将具体策略换成简单的函数,并去掉抽象基类.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Order: def __init__(self, customer, cart, promotion=None): self.customer = customer self.cart = cart self.promotion = promotion def total(self): if not hasattr(self, '__total'): self.__taotal = 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(self) return self.total() - discount def fidelity_promo(order): """策略一 作为函数""" return order.total() * 0.5 if order.customer.fiderlity >= 1000 else 0
def bulk_item_promo(order): """策略二""" discount = 0 for item in order.cart: if item.quantity >= 20: discount += item.total() * 0.1 return discount
|
这样我们的Order类使用起来也根据简单了, 构造Order实例时直接传入函数作为参数.
选择最佳策略
下面假设需要一个”元策略”, 来在所有具体策略中选择最优策略, 此时将函数作为对象很容易实现这一元策略(相比写一个类):
1 2 3 4 5 6 7
| promos = [fidelity_promo, bulk_item_promo]
def best_promo(order): return max(promo(order) for promo in promos)
|
这么写有一个弊端, 在于你需要不断维护策略列表,否则新加入的策略不会被考虑.
因此我们可以利用globals
函数来找出模块中的全部策略.
该函数会返回当前的全局符号表,然后我们在符号表中找到以_promo
结尾的函数.
1 2 3 4 5 6 7 8
| promos = [globals()[name] for name in globals() if name.endswith('_promo') and name != 'best_promo']
def best_promo(order): return max(promo(order) for promo in promos)
|
另一种写法是将所有具体策略函数写在一个单独的模块中(不包含元策略).然后利用inspect函数去找出该模块中所有函数.
1 2 3 4
| promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
|
命令模式
将函数作为参数传递同样可以简化命令模式. 命令模式解耦了调用操作的对象(调用者)和提供实现的对象(接受者). 该模式需要在二者之间放一个Command对象,它只有一个方法即执行的接口.这有些类似上面的策略模式
有时Command类较为复杂,需要保存自身的一些信息,此时我们可以通过给该类实现__call__
方法令其实例变成可调用对象.