程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

[Python] 使用一等函數實現設計模式

編輯:Python

《流暢的Python》盧西亞諾·拉馬略 第6章 使用一等函數實現設計模式  讀書筆記

6.1 案例分析:重構“策略”模式

如果合理利用作為一等對象的函數,某些設計模式可以簡化,“策略”模式就是其中一個例子。

6.1.1 經典的“策略”模式

《設計模式:可復用面向對象軟件的基礎》一書是這樣概述“策略”模式的:
定義一系列算法,把它們一一封裝起來,並且使它們可以相互替換。本模式使得算法可以獨立於使用它的客戶而變化。電商領域有個功能明顯可以使用“策略”模式,即根據客戶的屬性或訂單中的商品計算折扣。


上下文
把一些計算委托給實現不同算法的可互換組件,它提供服務。在這個電商示例中,上下文是 Order,它會根據不同的算法計算促銷折扣。
策略
實現不同算法的組件共同的接口。在這個示例中,名為 Promotion的抽象類扮演這個角色。
具體策略
策略的具體子類。fidelityPromo、BulkPromo 和LargeOrderPromo 是這裡實現的三個具體策略。

假如一個網店制定了下述折扣規則,假定一個訂單一次只能享用一個折扣
有 1000 或以上積分的顧客,每個訂單享 5% 折扣。
同一訂單中,單個商品的數量達到 20 個或以上,享 10% 折扣。
訂單中的不同商品達到 10 個或以上,享 7% 折扣。

【改進步驟】

示例 6-1 每個策略都是一個類,且只定義了一個方法 discount
示例 6-3 把具體策略換成了簡單的函數,而且去掉了 Promo 抽象類
示例 6-6 best_promo 迭代一個函數列表,並找出折扣額度最大的
         缺點:若想添加新的促銷策略,要定義相應的函數,還要記得把它添加到 promos 列表中
示例 6-7 使用 globals 函數幫助 best_promo 自動找到其他可用的*_promo 函數
示例 6-8 內省單獨的 promotions 模塊,構建 promos 列表
示例 7-3 promos 列表中的值使用 promotion 裝飾器填充 。傳送門 -> 函數裝飾器和閉包_wy_hhxx-----------------------------------------------------------------------------

示例 6-1 實現 Order 類,支持插入式折扣策略

order.py

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)
return self.__total
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):
"""返回折扣金額(正值)"""
class FidelityPromo(Promotion): # 第一個具體策略
"""為積分為1000或以上的顧客提供5%折扣"""
def discount(self, order):
return order.total() * .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() * .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() * .07
return 0

注意,line30-33 把 Promotion 定義為抽象基類(Abstract Base Class,ABC),這麼做是為了使用 @abstractmethod 裝飾器,從而明確表明所用的模式。

@abstractmethod:抽象方法,含abstractmethod方法的類不能實例化繼承了含abstractmethod方法的子類必須復寫所有abstractmethod裝飾的方法,未被裝飾的可以不重寫

示例 6-2 使用不同促銷折扣的 Order 類示例

>>> from order import *
>>> joe = Customer('John Doe', 0) #1
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5), #2
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>>
>>> Order(joe, cart, FidelityPromo()) #3
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, FidelityPromo()) #4
<Order total: 42.00 due: 39.90>
>>>
>>> banana_cart = [LineItem('banana', 30, .5), #5
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) #6
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0) #7
... for item_code in range(10)]
>>> Order(joe, long_order, LargeOrderPromo()) #8
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00>
>>>

說明:

#1 兩個顧客:joe 的積分是 0,ann 的積分是 1100。
#2 有三個商品的購物車。
#3 fidelityPromo 沒給joe提供折扣(積分<1000)。
#4 fidelityPromo 給ann提供了5%折扣(積分>1000)。
#5 banana_cart 中有30把香蕉和10個蘋果。
#6 BulkItemPromo 給joe購買的香蕉優惠了1.50美元(香蕉>20)。
#7 long_order 中有10個不同的商品,每個商品的價格為1.00美元。
#8 LargerOrderPromo 給joe的整個訂單提供了 7% 折扣(不同商品個數=10)。

6.1.2 使用函數實現“策略”模式

在示例 6-1 中,每個具體策略都是一個類,而且都只定義了一個方法,即 discount。此外,策略實例沒有狀態(沒有實例屬性)。
示例 6-3 是對示例 6-1的重構,把具體策略換成了簡單的函數,而且去掉了 Promo 抽象類

示例 6-3 Order 類和使用函數實現的折扣策略

……
class Order: # 上下文
……
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion(self) #計算折扣只需調用 self.promotion() 函數
return self.total() - discount
……
def fidelity_promo(order): #沒有抽象類,各個策略都是函數
"""為積分為1000或以上的顧客提供5%折扣"""
return order.total() * .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() * .1
return discount
def large_order_promo(order):
"""訂單中的不同商品達到10個或以上時提供7%折扣"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0

示例 6-4 使用函數實現的促銷折扣的 Order 類示例

>>> from order2 import *
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, fidelity_promo) #為了把折扣策略應用到 Order 實例上,只需把促銷函數作為參數傳入
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
>>>

沒必要在新建訂單時實例化新的促銷對象,函數拿來即用。
共享是推薦的做法,這樣不必在每個新的上下文(這裡是Order 實例)中使用相同的策略時不斷新建具體策略對象,從而減少消耗。

示例 6-6 best_promo 迭代一個函數列表,並找出折扣額度最大的

promos = [fidelity_promo, bulk_item_promo, large_order_promo] # promos 列出以函數實現的各個策略
def best_promo(order):
"""選擇可用的最佳折扣"""
return max(promo(order) for promo in promos) # 使用生成器表達式把 order 傳給 promos 列表中的各個函數,返回折扣額度最大的那個函數

測試如下,

>>> from order3 import *
>>>
>>> joe = Customer('John Doe', 0)
>>>
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> long_order = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>>
>>> Order(joe, long_order, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>

缺點:若想添加新的促銷策略,要定義相應的函數,還要記得把它添加到 promos 列表中;否則,當新促銷函數顯式地作為參數傳給 Order時,它是可用的,但是 best_promo 不會考慮它。

6.1.4 找出模塊中的全部策略

在 Python 中,模塊也是一等對象,而且標准庫提供了幾個處理模塊的函數。
內置函數globals() 返回一個字典,表示當前的全局符號表。這個符號表始終針對當前模塊(對函數或方法來說,是指定義它們的模塊,而不是調用它們的模塊)。
示例 6-7 使用 globals 函數幫助 best_promo 自動找到其他可用的*_promo 函數

promos = [globals()[name] for name in globals() #1
if name.endswith('_promo') #2
and name != 'best_promo'] #3
def best_promo(order):
"""選擇可用的最佳折扣"""
return max(promo(order) for promo in promos) #4

說明:
#1 迭代 globals() 返回字典中的各個 name。
#2 只選擇以 _promo 結尾的名稱。
#3 過濾掉 best_promo 自身,防止無限遞歸。
#4 best_promo 內部的代碼沒有變化。

>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'order4': <module 'order4' from '/root/pydir/order4.py'>}
>>>
>>> from order4 import *
>>>
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'order4': <module 'order4' from '/root/pydir/order4.py'>, 'ABC': <class 'abc.ABC'>, 'abstractmethod': <function abstractmethod at 0x7f8033ec6a60>, 'namedtuple': <function namedtuple at 0x7f802c0a4940>, 'Customer': <class 'order4.Customer'>, 'LineItem': <class 'order4.LineItem'>, 'Order': <class 'order4.Order'>, 'fidelity_promo': <function fidelity_promo at 0x7f802c0a5670>, 'bulk_item_promo': <function bulk_item_promo at 0x7f802c0a54c0>, 'large_order_promo': <function large_order_promo at 0x7f802c0a5430>, 'promos': [<function fidelity_promo at 0x7f802c0a5670>, <function bulk_item_promo at 0x7f802c0a54c0>, <function large_order_promo at 0x7f802c0a5430>], 'best_promo': <function best_promo at 0x7f802c0a53a0>}
>>>
>>> promos
[<function fidelity_promo at 0x7f802c0a5670>, <function bulk_item_promo at 0x7f802c0a54c0>, <function large_order_promo at 0x7f802c0a5430>]
>>>

收集所有可用促銷的另一種方法是,在一個單獨的模塊中保存所有策略函數,把 best_promo 排除在外。

示例 6-8 內省單獨的 promotions 模塊,構建 promos 列表

將全部三個promos函數放置到 promotions.py

order5.py 去除三個promos函數,並導入promotions 模塊和提供高階內省函數的 inspect 模塊

from abc import ABC, abstractmethod
from collections import namedtuple
import inspect
import promotions
……
promos = [func for name, func in
inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order):
"""選擇可用的最佳折扣"""
return max(promo(order) for promo in promos)

測試如下:

>>> import order5
>>> promos
[<function fidelity_promo at 0x7f802c0a5670>, <function bulk_item_promo at 0x7f802c0a54c0>, <function large_order_promo at 0x7f802c0a5430>]
>>>

6.2 命令模式

“命令”設計模式也可以通過把函數作為參數傳遞而簡化。
我們可以不為調用者提供一個 Command 實例,而是給它一個函數。此時,調用者不用調用 command.execute(),直接調用 command() 即可。MacroCommand 可以實現成定義了 __call__ 方法的類。這樣,MacroCommand 的實例就是維護一個函數列表的可調用對象。
示例 6-9 MacroCommand 的各個實例都在內部存儲著命令列表

class MacroCommand:
"""一個執行一組命令的命令"""
def __init__(self, commands):
self.commands = list(commands) #1
def __call__(self):
for command in self.commands: #2
command()

#1 使用 commands 參數構建一個列表,這樣能確保參數是可迭代對象,還能在各個 MacroCommand 實例中保存各個命令引用的副本
#2 調用 MacroCommand 實例時,self.commands 中的各個命令依序執行


  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved