学习《流畅的Python》第四天。耗时3天,终于写完了第5章的博客,可以开始写第6章的博客了,不过之前已经看了第6章一部分内容了。
设计模式 Page 141
《设计模式:可复用面向对象软件的基础》一书中有23个设计模式,其中有16个在动态语言中“不见了,或者简化了”。作者Peter Norvig讨论的是Lisp和Dylan,不过很多相关的动态特性在Python中也能找到。
Norvig建议在有一等函数的语言中,重新审视“策略”、“命令”、“模板方法”和“访问者”模式。通常,我们可以把这些模式中涉及的某些类的实例替换成简单的函数,从而减少样板代码。本章将使用函数对象重构“策略”模式,还将讨论一种更简单的方式,用于简化“命令”模式。
6.1.1 经典的“策略”模式 Page 142
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 * self.quantity
class Order: #上下文
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion.discount(self)
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
class Promotion(ABC): # 策略:抽象基类
@abstractmethod
def discount(self, order):
"""返回折扣金额(正值)"""
pass
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
注意,上例把Promotion
定义为抽象基类(Abstract Base Class, ABC),这么做是为了使用@abstractmethod
装饰器,从而明确表明所用的模式。
6.1.2 使用函数实现“策略”模式 Page 145
上例中每个具体策略都是一个类,而且都只定义了一个方法,即discount
。此外,策略实例没有状态(没有实例属性)。下面对上例进行重构,把具体策略换成了简单的函数,而且去掉了Promo
抽象类。
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 * self.quantity
class Order: # 上下文
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
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(self)
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
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) >= 10:
return order.total() * 0.07
return 0
6.1.3 选择最佳策略:简单的方式 Page 148
promos = [fidelity_promo, bulk_item_promo, large_order_promo]
def best_promo(order):
"""选择可用的最佳折扣"""
return max(promo(order) for promo in promos)
6.1.4 找出模块中的全部策略 Page 149
在Python中,模块也是一等对象,而且标准库提供了几个处理模块的函数。Python文档是这样说明内置函数globals
的。
globals()
返回一个字典,表示当前的全局符号表。这个符号表始终针对当前模块(对函数或方法来说,是指定义他们的模块,而不是调用它们的模块)。
下例使用globals
函数帮助best_promo
自动找到其它可用的*_promo函数,过程有点曲折。
promos = [globals()[name] for name in globals() if name.endswith('_promo') and name != 'best_promp']
def best_promo(order):
"""选择可用的最佳折扣"""
return max(promo(order) for promo in promos)
收集所有可用促销的另一种方法是,在一个单独的模块中保存所有策略函数,把best_promo
排除在外。
下例中,最大的变化是内省名为promotions
的独立模块,构建策略函数列表。注意,要导入promotions
模块,以及提供高阶内省函数的inspect
模块。
import promotions
import inspect
promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order):
"""选择可用的最佳折扣"""
return max(promo(order) for promo in promos)
inspect.getmembers
函数用于获取对象(这里是promotions
模块)的属性,第二个参数是可选的判断条件(一个布尔值函数)。我们使用的是inspect.isfunction
,只获取模块中的函数。
不管怎么命名函数,上例都可用;唯一重要的是,promotions
模块只能包含计算订单折扣的函数。当然,这是对代码的隐形假设。如果有人在promotions
模块中使用不同的签名定义函数,那么best_promo
函数尝试将其应用到订单上时会出错。
上例的目的不是提供完善的方案,而是强调模块内省的一种用途。
动态收集促销折扣函数更为显式的一种方案是使用简单的装饰器。
另外,书中还有6.2“命令”模式,写得比较简略,只有不到2页,就不总结了。