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