本章就Python中函数与设计模式做一些探讨, notebook文件在这里.
一等函数与设计模式
虽然设计模式与语言无关,但是某些设计模式并不适用于某些语言. 下面就Python中策略模式做一些讨论.
策略模式
在设计模式中,策略模式的概述如下:定义一系列算法,把它们一一封装起来,并且使它们可以互相替换.本模式使得算法可以独立于使用它的客户而变化.
这里举的例子是电商的促销策略,即根据客户的属性或订单中的商品计算折扣(顺便吐槽近几年双十一的折扣是越来越复杂了...).
例如某个网店制定了如下的折扣策略:1000积分以上客户,享受5%折扣
同一订单下,单个商品数量达到20个或以上,享受10%折扣
订单的不同产品达到10个或以上,享受7%折扣
那么按照策略模式,我们需要有如下的类:
上下文把一些计算委托给实现的不同算法的可互换组件,它提供服务. 在该例子中,上下文即为订单Order,它会根据不同算法计算促销折扣.
策略实现不同算法的组建的共同接口. 在该例子中,即为Promotion.
具体策略策略的具体子类. 在该例子中,即为上述的三种折扣策略.
具体策略由上下文类的客户选择, 在此例子中,我们可以在实例化订单类之前(__init__)就以某种方式选择好策略, 然后将其作为Order类初始化的参数.
经典策略模式
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装饰器来装饰抽象方法,没有具体实现该抽象方法的类无法被实例化.
虽然上面的实现没有问题,但是利用函数作为对象,我们可以更加精简地完成这一需求.
利用函数实现策略模式
回顾上面的实现,可以发现貌似我们的具体策略只有一个方法,似乎没有必要将其写成一个类.因此我们可以将具体策略换成简单的函数,并去掉抽象基类.
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实例时直接传入函数作为参数.
选择最佳策略
下面假设需要一个"元策略", 来在所有具体策略中选择最优策略, 此时将函数作为对象很容易实现这一元策略(相比写一个类):
#策略列表
promos = [fidelity_promo, bulk_item_promo]
def best_promo(order):
return max(promo(order) for promo in promos)
这么写有一个弊端, 在于你需要不断维护策略列表,否则新加入的策略不会被考虑.
因此我们可以利用globals函数来找出模块中的全部策略.
该函数会返回当前的全局符号表,然后我们在符号表中找到以_promo结尾的函数.
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函数去找出该模块中所有函数.
# 下面的promotions是一个模块
promos = [func for name, func in
inspect.getmembers(promotions, inspect.isfunction)]
命令模式
将函数作为参数传递同样可以简化命令模式. 命令模式解耦了调用操作的对象(调用者)和提供实现的对象(接受者). 该模式需要在二者之间放一个Command对象,它只有一个方法即执行的接口.这有些类似上面的策略模式
有时Command类较为复杂,需要保存自身的一些信息,此时我们可以通过给该类实现__call__方法令其实例变成可调用对象.