Python中備忘功能和裝飾器

PyPer發表於2015-03-05

“備忘”的定義

“memoization”(備忘)這個詞是由Donald Michie在1968年提出的,它基於拉丁語單詞“memorandum”(備忘錄),意思是“被記住”。雖然它和單詞“memorization”在某種程度上有些相似,但它並不是該單詞的錯誤拼寫。實際上,Memoisation是一種用於通過計算來加速程式的技術,它通過記住輸入量的計算結果,例如函式呼叫結果,來實現其加速目的。如果遇到相同的輸入或者具有相同引數的函式呼叫,那麼之前儲存的結果就可以被再次使用,從而避免一些不必要的計算。在很多情況下,可以使用一個簡單的陣列來儲存結果,但也可以使用許多其他的資料結構,例如關聯陣列,它在Perl語言中叫做雜湊,在Python語言中稱為字典。

備忘功能可以由程式設計師顯式地程式設計實現,但是一些程式語言如Python,都提供了自動備忘函式的機制。

利用函式裝飾器實現備忘功能

在前面關於遞迴函式的那章中,我們分別使用迭代和遞迴實現了斐波納契數列的求解。我們已經證明,如果直接利用斐波納契數列的數學定義,在一個遞迴函式中實現數列的求解,正如下面的函式一樣,那麼它將具有指數級的時間複雜度:

此外,我們還提出了一種提高遞迴實現的時間複雜度的方法,即通過新增一個字典來記住之前函式的計算結果。這是一個顯式地使用備忘技術的例子,只是當時我們並沒有這麼稱呼它。這種方法的缺點是,原始遞迴實現的明晰性和優雅性丟失了。

造成以上缺點的原因是,我們改變了遞迴函式fib的程式碼。不過下面的程式碼不會改變我們的fib函式,所以它的明晰性和易讀性並沒有丟失。為了實現該目的,我們使用自定義的函式memoize()。函式memoize()以函式作為引數,並使用一個字典“memo”來儲存函式的結果。雖然變數“memo”和函式“f”僅僅具有區域性備忘功能,但是它們通過函式“helper”被一個閉包捕獲,而memoize()將函式“helper”作為引用返回。所以,對memoize(fib)的呼叫將會返回一個helper()的引用,而在helper()中實現了fib()函式的功能以及一個用於儲存還未儲存的結果到字典“memo”中的包裝器,並防止重新計算“memo”中已有的結果。

現在讓我們瞭解下所謂的裝飾器,首先看一下上面程式碼中將備忘功能指派到fib函式的這一行:

一種說法是,函式memoize()裝飾了函式fib。

將Memoize封裝成類

我們還可以將結果的快取封裝到一個類中,如下面的例子所示:

因為我們使用了字典,所以不能使用可變引數,即引數必須是不可變的。

Python中的裝飾器

Python中的裝飾器是一個可呼叫的Python物件,用於修改一個函式、方法或者類的定義。原始的物件,也就是即將被改變的那個物件,作為引數傳遞給一個裝飾器,而裝飾器則返回一個修改過的物件,例如一個修改過的函式,它會被繫結到定義中使用的名字上。Python中的裝飾器與Java中的註解有一個相似的語法,即Python中的裝飾器語法可以看作是純粹的語法糖,使用“@”作為關鍵字。

示例:使用裝飾器實現備忘功能

其實,前面我們已經使用了裝飾器,只是沒有這麼稱呼它而已。實際上,本章開頭例子中的memoize函式就是一個裝飾器,我們使用它來記住fib函式的結果,只是我們沒有使用Python中裝飾器特殊的語法而已,即艾特字元“@”。

相比於寫成下面的形式

我們可以這樣寫

但這一行必須直接寫在被裝飾的函式之前,在我們的例子fib()中,如下所示:

利用裝飾器檢查引數

在講解遞迴函式的那章中我們介紹了階乘函式,在那裡我們希望保持函式儘可能簡單,而不想掩蓋基本理念,所以程式碼中沒有包含任何引數檢查程式碼。然而,如果別人以負數或者浮點數作為引數來呼叫我們的函式,那麼函式將會陷入一個死迴圈。

下面的程式使用一個裝飾器函式來確保傳給函式“factorial”的引數是一個正整數:

練習

1、我們的練習是一個古老的謎題。1612年,法國耶穌會士Claude-Gaspar Bachet提出了該謎題,即使用一個天平稱出從1磅到40磅的所有整數重量的東西(例如,糖或者麵粉),求最少的砝碼數量。

第一個方法可能是使用1、2、4、8、16和32磅重量的這些砝碼。如果我們將砝碼放在天平的一端,而將物品放在另一端,那麼這種方法用到的砝碼數量將是最小的。然而,我們也可以將砝碼同時放在天平的兩端,此時我們僅僅需要重量為1、3、9、27的砝碼。

編寫一個Python函式weigh(),該函式計算需要的砝碼以及它們在天平盤中的分佈,以此來稱量1磅到40磅中任何一個整數重量的物品。

解決方法

1、我們需要前面章節“Linear Combinations”中的函式linear_combination()。

2、利用上面的程式碼,就能很容易寫出我們的函式weigh()。

相關文章