摘要:本篇主要講述中使用函數來實現策略模式和命令模式,最后總結出這種做法背后的思想。
《流暢的Python》筆記。1. 重構策略模式本篇主要講述Python中使用函數來實現策略模式和命令模式,最后總結出這種做法背后的思想。
策略模式如果用面向對象的思想來簡單解釋的話,其實就是“多態”。父類指向子類,根據子類對同一方法的不同重寫,得到不同結果。
1.1 經典的策略模式下圖是經典的策略模式的UML類圖:
《設計模式:可復用面向對象軟件的基礎》一書這樣描述策略模式:
定義一系列算法,把它們封裝起來,且使它們能相互替換。本模式使得算法可獨立于使用它的客戶而變化。
下面以一個電商打折的例子來說明策略模式,打折方案如下:
有1000及以上積分的顧客,每個訂單享5%優惠;
同一訂單中,每類商品的數量達到20個及以上時,該類商品享10%優惠;
訂單中的不同商品達10個及以上時,整個訂單享7%優惠。
為此我們需要創建5個類:
Order類:訂單類,相當于上述UML圖中的Context上下文;
Promotion類:折扣類的父類,相當于UML圖中的Strategy策略類,實現不同策略的共同接口;
具體策略類:FidelityPromo,BulkPromo和LargeOrderPromo依次對應于上述三個打折方案。
以下是經典的策略模式在Python中的實現:
from abc import ABC, abstractmethod from collections import namedtuple Customer = namedtuple("Customer", "name fidelity") class LineItem: # 單個商品 def __init__(self, product, quantity, price): self.produce = 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) # 形參cart中的元素是LineItem 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 class Promotion(ABC): # 策略:抽象基類 @abstractmethod # 抽象方法 def discount(self, order): """返回折扣金額(正值)""" 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
該類的使用示例如下:
>>> ann = Customer("Ann Smith", 1100) >>> joe = Customer("John Joe", 0) >>> cart = [LineItem("banana", 4, 0.5), LineItem("apple", 10, 1.5), ... LineItem("watermellon", 5, 5.0)] >>> Order(ann, cart, FidelityPromo()) # 每次新建一個具體策略類 >>> Order(joe, cart, FidelityPromo())1.2 Python函數重構策略模式
現在用Python函數以更少的代碼來重構上述的策略模式,去掉了抽象類Promotion,用函數代替具體的策略類:
# 不用導入abc模塊,去掉了Promotion抽象類; # Customer, LineItem不變,Order類只修改due()函數;三個具體策略類改為函數 -- snip -- class Order: -- snip -- def due(self): # 折扣 if self.promotion is None: discount = 0 else: discount = self.promotion(self) # 修改為函數 return self.total() - discount 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_items) >= 10: return order.total() * 0.07 return 0
該類現在的使用示例如下:
>>> Order(ann, cart, fidelity_promo) # 沒有實例化新的促銷對象,函數拿來即用
脫離Python語言環境,從面相對象編程來說:
1.1中的使用示例可以看出,每次創建Order類時,都創建了一個具體策略類,即使不同的訂單都用的同一個策略。按理說它們應該共享同一個具體策略的實例,但實際并沒有。這就是策略模式的一個弊端。為了彌補這個弊端,如果具體的策略沒有維護內部狀態,你可以為每個具體策略創建一個實例,然后每次都傳入這個實例,這就是單例模式;但如果要維護內狀態,就需要將策略模式和享元模式結合使用,這又提高了代碼行數和維護成本。
在Python中則可以用函數來避開策略模式的這些弊端:
不用維護內部狀態時,我們可以直接用一般的函數;如果需要維護內部狀態,可以編寫裝飾器(裝飾器也是函數);
相對于編寫一個抽象類,再實現這個抽象類的接口來說,直接編寫函數更方便;
函數比用戶定義的類的實例更輕量;
無需去實現享元模式,每個函數在Python編譯模塊時只會創建一次,函數本身就是可共享的對象。
1.3 自動選擇最佳策略上述代碼中,我們需要自行傳入打折策略,但我們更希望的是程序自動選擇最佳打折策略。以下是我們最能想到的一種方式:
# 在生成Order實例時,傳入一個best_promo函數,讓其自動選擇最佳策略 promos = [fidelity_promo, bulk_item_promo, large_order_promo] # 三個打折函數的列表 def best_promo(order): """選擇可用的最佳策略""" return max(promo(order) for promo in promos)
但這樣做有一個弊端:如果要新增打折策略,不光要編寫打折函數,還得把函數手動加入到promos列表中。我們希望程序自動識別這些具體策略。改變代碼如下:
promos = [globals()[name] for name in globals() if name.endswith("_promo") and name != "best_promo"] # 自動獲取當前模塊中的打折函數 def best_promo(order): -- snip --
在Python中,模塊也是一等對象,globals()函數是標準庫提供的處理模塊的函數,它返回一個字典,表示當前全局符號表。這個符號表始終針對當前模塊(對函數或方法來說,是指定義它們的模塊,而不是調用它們的模塊)
如果我們把各種具體策略多帶帶放到一個模塊中,比如放到promotions模塊中,上述代碼還可改為如下形式:
# 各具體策略多帶帶放到一個模塊中 import promotions, inspect # inspect.getmembers函數用于獲取對象的屬性,第二個參數是可選的判斷條件 promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)] def best_promo(order): -- snip --
其實,動態收集具體策略函數更為顯式的一種方案是使用簡單的裝飾器,這將在下一篇中介紹。
2. 命令模式命令模式的UML類圖如下:
命令模式的目的是解耦發起調用的對象(調用者,Caller)和提供實現的對象(接受者,Receiver)。實際做法就是在它們之間增加一個命令類(Command),它只有一個抽象接口execute(),具體命令類實現這個接口即可。這樣調用者就無需了解接受者的接口,不同的接受者還可以適應不同的Command子類。
有人說“命令模式是回調機制的面向對象替代品”,但問題是,Python中我們不一定需要這個替代品。具體說來,我們可以不為調用者提供一個Command實例,而是給它一個函數。此時,調用者不用調用command.execute(),而是直接command()。
以下是一般的命令模式代碼:
from abc import ABC, abstractmethod class Caller: def __init__(self, command=None): self.command = command def action(self): """把對接受者的調用交給中介Command""" self.command.execute() class Receiver: def do_something(self): """具體的執行命令""" print("I"m a receiver") class Command(ABC): @abstractmethod def execute(self): """調用具體的接受者方法""" class ConcreteCommand(Command): def __init__(self, receiver): self.receiver = receiver def execute(self): self.receiver.do_something() if __name__ == "__main__": receiver = Receiver() command = ConcreteCommand(receiver) caller = Caller(command) caller.action() # 結果: I"m a receiver
直接將上述代碼改成函數的形式,其實并不容易改寫,因為具體的命令類還保存了接收者。但是換個思路,將其改成可調用對象,那么代碼就可以變成如下形式:
class Caller: def __init__(self, command=None): self.command = command def action(self): # 之前是self.command.execute() self.command() class Receiver: def do_something(self): """具體的執行命令""" print("I"m a receiver") class ConcreteCommand: def __init__(self, receiver): self.receiver = receiver def __call__(self): self.receiver.do_something() if __name__ == "__main__": receiver = Receiver() command = ConcreteCommand(receiver) caller = Caller(command) caller.action()3. 總結
看完這兩個例子,不知道大家發現了什么相似之處了沒有:
它們都把實現單方法接口的類的實例替換成了可調用對象。畢竟,每個Python可調用對象都實現了單方法接口,即__call__方法。
直白一點說就是,如果你定義了一個抽象類,這個類只有一個抽象方法a(),然后還要為這個抽象類派生出一大堆具體類來重寫這個方法a(),那么此時大可不必定義這個抽象類,直接將這些具體類改寫成可調用對象即可,在__call__方法中實現a()要實現的功能。
這相當于用Python中可調用對象的基類充當了我們定義的基類,我們便不用再定義基類;對抽象方法a()的重寫變成了對特殊方法__call__的重寫,畢竟我們只是想要這些方法有一個相同的名字,至于叫什么其實無所謂。
迎大家關注我的微信公眾號"代碼港" & 個人網站 www.vpointer.net ~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/44727.html
摘要:函數內省的內容到此結束。函數式編程并不是一個函數式編程語言,但通過和等包的支持,也可以寫出函數式風格的代碼。 《流暢的Python》筆記。本篇主要講述Python中函數的進階內容。包括函數和對象的關系,函數內省,Python中的函數式編程。 1. 前言 本片首先介紹函數和對象的關系;隨后介紹函數和可調用對象的關系,以及函數內省。函數內省這部分會涉及很多與IDE和框架相關的東西,如果平時...
摘要:什么是前端工程師總而言之就是運用等技術,在工作中配合設計師實現用戶界面,和后端工程師進行數據對接,完成應用開發的職位。 什么是前端工程師?總而言之,就是運用 HTML、CSS、JavaScript 等 Web 技術,在工作中配合UI設計師實現用戶界面,和后端工程師進行數據對接,完成 Web 應用開發的職位。Tips:個人博客排版、UI更佳;地址:https://haonancx.git...
摘要:什么是前端工程師總而言之就是運用等技術,在工作中配合設計師實現用戶界面,和后端工程師進行數據對接,完成應用開發的職位。 什么是前端工程師?總而言之,就是運用 HTML、CSS、JavaScript 等 Web 技術,在工作中配合UI設計師實現用戶界面,和后端工程師進行數據對接,完成 Web 應用開發的職位。Tips:個人博客排版、UI更佳;地址:https://haonancx.git...
摘要:什么是前端工程師總而言之就是運用等技術,在工作中配合設計師實現用戶界面,和后端工程師進行數據對接,完成應用開發的職位。 什么是前端工程師?總而言之,就是運用 HTML、CSS、JavaScript 等 Web 技術,在工作中配合UI設計師實現用戶界面,和后端工程師進行數據對接,完成 Web 應用開發的職位。Tips:個人博客排版、UI更佳;地址:https://haonancx.git...
摘要:一等函數在中,函數是一等對象。匿名函數關鍵字在表達式內創建匿名函數然而,簡單的句法限制了函數的定義體只能使用純表達式,即函數的定義體中不能賦值,不能使用等語句。匿名函數適合用于作為函數的參數 一等函數 在python中,函數是一等對象。編程語言理論家把一等對象定義為滿足以下條件的程序實體: 在運行時創建 能賦值給變量或數據結構中的元素 能作為參數傳給函數 能作為函數的返回結果 在p...
閱讀 1694·2023-04-25 20:16
閱讀 3878·2021-10-09 09:54
閱讀 2710·2021-09-04 16:40
閱讀 2527·2019-08-30 15:55
閱讀 843·2019-08-29 12:37
閱讀 2746·2019-08-26 13:55
閱讀 2917·2019-08-26 11:42
閱讀 3159·2019-08-23 18:26