Python的內鍵字典型別(Dictionary Type)對於維護一個物件生命期的動態內部狀態來說,非常useful。
舉例來說,,假如我們有一個銀行帳本,須要記錄客戶的存款與記錄,但我們事先並不知道他們的姓名。
那我們可能設計以下類別,透過字典來將這些名字記錄起來:
class BankBook( object ):
def __init__(self):
self._history = {}
def add_customer(self, name):
self._history[name] = []
def save_money(self, name, amount):
self._history[name].append(amount)
def draw_money(self, name, amount):
self._history[name].append(-1 * amount)
def show_balance(self, name):
moneys = self._history[name]
return sum(moneys)
這個類別的使用方式很簡單:
book = BankBook()
book.add_customer('paul')
book.save_money('paul', 1200)
book.save_money('paul', -200)
print(book.show_balance('paul'))
字典因為太容易使用,以至於有過度擴充它們而寫出脆弱code的風險
例如,我們面臨了一個新的需求而要擴充這個類別,誐們希望記錄不同幣別的帳戶,我們可以修改看看,試著將_history也變成字典,用於區分不同的幣別
class CurrencyBankBook( object ):
def __init__(self):
self._history = {}
def add_customer(self, name):
self._history[name] = {} #原本串列變成字典
def save_money(self, name, currency_name , amount):
by_currency = self._history[name]
currency_book_list = by_currency.setdefault(currency_name, [])
currency_book_list.append(amount)
def draw_money(self, name, currency_name, amount):
self.save_money(name, currency_name, -1 * amount)
def show_balance(self, name, currency_name):
if not currency_name:
raise ValueError("currency_name can't be empty")
currency_book_list = self._history[name]
if isinstance(currency_book_list, dict):
if currency_name in currency_book_list.keys():
return sum(currency_book_list[currency_name])
raise ValueError( "currency_name not exist!!" )
這看起來變化不大,只是在於history的處理與查找上增加了一點複雜度,但仍在可以處理的範圍內,使用方式依然很簡單。
book = CurrencyBankBook()
book.add_customer('paul')
book.save_money('paul', 'ntd', 1200)
book.draw_money('paul', 'ntd', 1000)
book.save_money('paul', 'us', 300)
book.draw_money('paul', 'us', 200)
print("ntd-balance:%s" % book.show_balance('paul', 'ntd'))
print("us-balance:%s" % book.show_balance('paul', 'us'))
現在想像一下,如果我們的需求再次改變了,目前我們想要記錄每一次存款的時間點與備註,像網路銀行查詢帳戶記錄的時候,都會有每個交易時間點的餘額資訊
實作這種功能的方式之一就是變更最內層的字典,將幣別映射到的金額使用元組(tuple)來作為值。
我們改寫類別如下:
class AdvCurrencyBankBook( object ):
def __init__(self):
self._history = {}
def add_customer(self, name):
self._history[name] = {} #原本串列變成字典
def save_money(self, name, currency_name , amount, time=None, memo=""):
by_currency = self._history[name]
currency_book_list = by_currency.setdefault(currency_name, [])
currency_book_list.append((amount, datetime.now() if time == None else time, memo))
def draw_money(self, name, currency_name, amount, time=None, memo=""):
self.save_money(name, currency_name,-1 * amount, time, memo)
def show_balance(self, name, currency_name, balance_time=datetime(2017, 9, 22, 10, 55, 00)):
if not currency_name:
raise ValueError("currency_name can't be empty")
currency_book_list = self._history[name]
if isinstance(currency_book_list, dict):
if currency_name in currency_book_list.keys():
return sum([amount for amount, datetime, memo in currency_book_list[currency_name] if datetime <= balance_time])
raise ValueError( "currency_name not exist!!" )
book = AdvCurrencyBankBook()
book.add_customer('paul')
book.save_money('paul', 'ntd', 1200, time=datetime(2017, 9, 21, 10, 55, 00))
book.draw_money('paul', 'ntd', 1000, time=datetime(2017, 9, 22, 10, 55, 00))
book.save_money('paul', 'us', 300, time=datetime(2017, 9, 21, 10, 55, 00))
book.draw_money('paul', 'us', 200, time=datetime(2017, 9, 22, 10, 55, 00))
print("ntd-balance:%s" % book.show_balance('paul', 'ntd', balance_time=datetime(2017, 9, 21, 23, 59, 59)))
print("us-balance:%s" % book.show_balance('paul', 'us', balance_time=datetime(2017, 9, 21, 23, 59, 59)))
雖然對於save_money跟draw_money來說,只是多加一個參數,並把值變成一個元組(tuple)。但現在balance開始要用迴圈開始查找條件資料。這個類別使用方式也更加困難了。
包含位置引數所代表的意義,並不容易弄清楚,例如:book.draw_money(‘paul’, ‘us’, 200, datetime(2017, 9, 22, 10, 55, 00))
當我們看到這樣的複雜情況出現時,就知道該是時候放棄字典與元組了,應是改為使用型別階層架構的類別的時候了
當我們發現記錄工作變得複雜,就將它拆解成多個類別。這讓我們得以提供定義更好的介面,用較好的方式來封裝資料。
重構一下吧!
Effective Python建議我們可以從依存樹(dependency tree)的底部開始改寫為類別,也就是單一次交易,對於這種簡單的資訊來說,使用一個類別似乎太過,而一個元組(tuple)看起來好像比較適當
if isinstance(currency_book_list, dict):
if currency_name in currency_book_list.keys():
return sum([amount for amount, datetime, _ in currency_book_list[currency_name] if datetime <= balance_time])
問題在於一般的元組是位置型的指派方式。當我們想要更多的資訊關聯到幣別帳戶,像是我們已經定義的memo備註,就得重新再改寫那個三元組。讓程式碼知有n個項目存在
在此我們可以使用_(底線變數名稱為,來unpack並直接skip掉(撈英文?)
這種將元組擴充得來越長的模式,就類似越來越多層的字典。只要發現元組的長度超過了二元組,就應該考慮namedtuple了
collections模組內的namedtuple可以位置引數或關鍵字引數的方式來建構,讓欄位可以以具名的屬性來存取
import collections
Transaction = collections.namedtuple('Transaction', ('amount', 'time', 'memo'))
def save_money(self, name, currency_name , amount, time=None, memo=""):
by_currency = self._history[name]
currency_book_list = by_currency.setdefault(currency_name, [])
currency_book_list.append(Transaction(amount, datetime.now() if time == None else time, memo))
def show_balance(self, name, currency_name, balance_time=datetime(2017, 9, 22, 10, 55, 00)):
if not currency_name:
raise ValueError("currency_name can't be empty")
currency_book_list = self._history[name]
total = 0
if isinstance(currency_book_list, dict):
for data in currency_book_list[currency_name]:
if data.time <= balance_time:
total += data.amount
return total
raise ValueError( "currency_name not exist!!" )
namedtuple比tuple更具體,使用上可以寫出較易維護的程式碼,但是仍有其限制
- 無法為namedtuple類別指定預設的引數。如果資料特性有許多選擇性的特性。那這就會使得它們變的過於笨重。
- namedtuple實體的屬性仍然可透過數值的索引與迭代來存取。特別是在對外提供的api中,這可會導致非預期的使用,讓你在之後更能以改寫成一個真正的類別。
from datetime import datetime
import collections
Transaction = collections.namedtuple( 'Transaction', ('amount', 'time', 'memo') )
class Account( object ):
def __init__(self):
self._history = []
def save_money(self, amount, time=None, memo=""):
self._history.append( Transaction( amount, datetime.now() if time == None else time, memo ) )
def draw_money(self, amount, time=None, memo=""):
self.save_money( -1 * amount, time, memo )
def get_balance(self, balance_time=datetime.now()):
total = 0
for transaction in self._history:
if transaction.time <= balance_time:
total += transaction.amount
return total
class Customer( object ):
def __init__(self):
self.__account = {}
def currency_account(self, currency_name):
if currency_name not in self.__account:
self.__account[currency_name] = Account()
return self.__account[currency_name]
def show_balance(self):
for name, account in self.__account.items():
print( "%s-balance:%s" % (name, account.get_balance(datetime.now())) )
class ObjectBankBook( object ):
def __init__(self):
self.__customer = {}
def customer(self, name):
if name not in self.__customer:
self.__customer[name] = Customer()
if isinstance( self.__customer[name], Customer ):
return self.__customer[name]
這些類別的行數幾乎是之前實作的double,但程式碼比較容易閱讀與維護。
book = ObjectBankBook()
paul = book.customer( 'paul' )
usAccount = paul.currency_account('us')
usAccount.save_money(1200)
usAccount.draw_money(300)
ntdAccount = paul.currency_account('ntd')
ntdAccount.save_money(400)
ntdAccount.draw_money(200)
paul.show_balance()
us-balance:900
ntd-balance:200
註:如果必要,你可以撰寫回溯相容的方法來協助將舊API的使用方式移植到物件階層架構的新方式。