本章就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
# 下面的promotions是一个模块  
promos = [func for name, func in
inspect.getmembers(promotions, inspect.isfunction)]

命令模式

将函数作为参数传递同样可以简化命令模式. 命令模式解耦了调用操作的对象(调用者)和提供实现的对象(接受者). 该模式需要在二者之间放一个Command对象,它只有一个方法即执行的接口.这有些类似上面的策略模式

有时Command类较为复杂,需要保存自身的一些信息,此时我们可以通过给该类实现__call__方法令其实例变成可调用对象.