Python的Functools模塊簡介

模塊中有什么?
functools模塊是Python的標(biāo)準(zhǔn)庫的一部分,它是為高階函數(shù)而實(shí)現(xiàn)的。高階函數(shù)是作用于或返回另一個(gè)函數(shù)或多個(gè)函數(shù)的函數(shù)。一般來說,對這個(gè)模塊而言,任何可調(diào)用的對象都可以作為一個(gè)函數(shù)來處理。
functools 提供了 11個(gè)函數(shù):
cached_property()
cmp_to_key()
lru_cache()
partial()
partialmethod()
reduce()
singledispatch()
singledispatchmethod()
total_ordering()
update_wrapper()
wraps()
在整篇文章中,我們將更深入地研究每個(gè)函數(shù),并給出一些有用的示例。你可以在GitHub上找到文章中使用的代碼片段。享受吧!
備注:本文基于Python 3.8.2 (CPython)。有些函數(shù)在CPython的早期版本中可能不存在。
functools中 的函數(shù)
@cached_property - 緩存實(shí)例方法
想象一下,你有一個(gè)大型數(shù)據(jù)集,為了分析它,你實(shí)現(xiàn)了一個(gè)保存整個(gè)數(shù)據(jù)集的類。此外,你還實(shí)現(xiàn)了一些函數(shù)來計(jì)算諸如手頭數(shù)據(jù)集的標(biāo)準(zhǔn)偏差之類的信息。問題:你每次調(diào)用該方法時(shí),它都會(huì)重新計(jì)算標(biāo)準(zhǔn)偏差—這需要時(shí)間啊!這就是@cached_property派上用場的地方了。
它的目的是將類的一個(gè)方法轉(zhuǎn)換為一個(gè)屬性,該屬性的值只計(jì)算一次,然后被緩存為實(shí)例生命周期中的一個(gè)普通屬性。其行為與內(nèi)置的@property 裝飾器[2]非常相似,只是增加了緩存過程。讓我們來看一下來自Python文檔中的例子:

在當(dāng)前的場景中,我們在一個(gè)DataSet實(shí)例中存儲(chǔ)了一個(gè)(很大的)數(shù)字序列。此外,我們還定義了兩個(gè)方法,分別用來計(jì)算標(biāo)準(zhǔn)偏差和方差。我們將@cached_property裝飾器[3]分別應(yīng)用于這兩個(gè)函數(shù),以將它們轉(zhuǎn)換為緩存屬性。這意味著值確實(shí)只計(jì)算了一次,然后就被緩存了。
備注:DataSet的每個(gè)實(shí)例都需要有一個(gè)帶有不可變映射的__dict__屬性。這是裝飾器能夠正確工作所必需的。
cmp_to_key() - 一個(gè)轉(zhuǎn)換函數(shù)
在繼續(xù)之前,我們首先需要理解比較函數(shù)和鍵函數(shù)之間的區(qū)別。
比較函數(shù)是任何可調(diào)用的對象,它會(huì)接受兩個(gè)參數(shù),比較它們并根據(jù)所提供的參數(shù)順序返回一個(gè)數(shù)字。負(fù)數(shù)表示第一個(gè)參數(shù)小于第二個(gè)參數(shù),零表示它們相等,正數(shù)表示第一個(gè)參數(shù)大于第二個(gè)參數(shù)。Python中的一個(gè)簡單實(shí)現(xiàn)可能是這樣的:

相反,鍵函數(shù)是一個(gè)可調(diào)用對象,它接受一個(gè)參數(shù)并返回另一個(gè)用作排序鍵的值。這個(gè)分組的一個(gè)突出代表是operator.itemgetter()鍵函數(shù)[4],你可能從日常的編碼中已經(jīng)了解了它。鍵函數(shù)通常會(huì)被提供給像sort()、min()和max()之類的內(nèi)置函數(shù)。
實(shí)際上,cmp_to_key()會(huì)將一個(gè)比較函數(shù)轉(zhuǎn)換為一個(gè)鍵函數(shù)。cmp_to_key()函數(shù)的實(shí)現(xiàn)是為了支持從Python2到Python3的轉(zhuǎn)換,因?yàn)樵赑ython2中存在一個(gè)用于比較和排序的名為cmp()的函數(shù)(以及一個(gè)雙下劃線方法__cmp__())。
@lru_cache() - 通過緩存增加代碼性能
@lru_cache()是一個(gè)裝飾器,它用一個(gè)記憶化的可調(diào)用對象來包裝一個(gè)函數(shù),這個(gè)可調(diào)用對象可以保存最近的maxsize次調(diào)用(默認(rèn)值:128)。
備注:簡單來說,記憶化意味著保存一個(gè)函數(shù)調(diào)用的結(jié)果,如果這個(gè)函數(shù)再次使用相同的參數(shù)被調(diào)用時(shí),則返回該結(jié)果。有關(guān)更多信息,請參閱Dan Bader關(guān)于Python中記憶化的文章[5]。
如果你有昂貴的或I/O綁定的函數(shù),而這些函數(shù)會(huì)被周期性地使用相同的參數(shù)進(jìn)行調(diào)用,那么這一點(diǎn)特別有用。LRU緩存代表最近最少使用的緩存,指的是這樣一個(gè)緩存,它會(huì)在條目達(dá)到最大大小時(shí)刪除最近最少使用的元素。如果maxsize設(shè)置為None,則LRU特性會(huì)被禁用。
讓我們來看兩個(gè)例子。在第一個(gè)示例中,我們定義了一個(gè)函數(shù)get_pep(),它接受一個(gè)PEP編號(Python增強(qiáng)提案)并返回這個(gè)PEP的內(nèi)容,如果該P(yáng)EP存在的話。

如你所見,我們將@lru_cache()裝飾器添加到了函數(shù)中,并將緩存的最大大小設(shè)置為32。我們在使用許多PEP一個(gè)for循環(huán)中調(diào)用get_pep()。如果你仔細(xì)查看list_of_peps,你可以看到有兩個(gè)數(shù)字在列表中出現(xiàn)了兩次甚至三次:8和320。
一旦你執(zhí)行了這個(gè)腳本,你就會(huì)發(fā)現(xiàn)所獲取的PEP會(huì)在不打印出其URL的情況下立即出現(xiàn),這些PEP我們已經(jīng)從python.org請求過了。這是由于我們沒有調(diào)用函數(shù)并再次從網(wǎng)站獲取它,而是從我們的緩存中獲取它。

在這個(gè)腳本的最后,我們打印了get_pep()的緩存信息。這表明我們有三次命中,這意味著Python使用了一個(gè)緩存值三次,而不是再次調(diào)用該函數(shù)(一次使用數(shù)字8,兩次使用320)。另外8次調(diào)用未命中,因此調(diào)用了函數(shù)并將結(jié)果添加到了緩存中。因此,最終的緩存由8個(gè)條目組成。
在第二個(gè)例子中,我們有一個(gè)想要加速的斐波那契數(shù)列的遞歸實(shí)現(xiàn)。

在這個(gè)例子中,我們計(jì)算了一個(gè)長度為16的斐波那契數(shù)列,并打印生成的序列以及fib()函數(shù)的緩存信息。

你可能會(huì)對緩存的命中次數(shù)和未命中次數(shù)感到驚訝。但是,請考慮以下情況:首先,我們計(jì)算n=0時(shí)的結(jié)果。因?yàn)槲覀兊木彺嬷羞€沒有條目,所以需要計(jì)算結(jié)果,這將使未命中增加1,并導(dǎo)致hits=0 和 misses=1。當(dāng)你以n=1調(diào)用fib()時(shí),又會(huì)出現(xiàn)這種情況。接著,fib()會(huì)被以n=2調(diào)用。我們通過計(jì)算n=1和n=0的結(jié)果并將它們相加來遞歸地計(jì)算結(jié)果。我們已經(jīng)計(jì)算了這兩個(gè)結(jié)果,所以我們可以從緩存中獲取它們。因此,我們只有一個(gè)新的未命中,因?yàn)槲覀冞€沒有n=2的條目。這個(gè)過程會(huì)一直持續(xù)到所有16個(gè)n都被傳遞給fib(),最后的結(jié)果只有16次未命中。
你想知道在本例中我們使用@lru_cache()節(jié)省了多少時(shí)間嗎?我們可以使用Python的timeit .timeit()函數(shù)來測試它,這個(gè)函數(shù)會(huì)向我們展示一些不可思議的東西:

通過使用 @lru_cache(),fib()函數(shù)快了約100000倍-哇偶!這絕對是一個(gè)你想記住的裝飾器。
@total_ordering - 通過使用裝飾器來減少代碼行數(shù)
用Python編程通常需要編寫自己的類。在某些情況下,你希望能夠比較該類的不同實(shí)例。根據(jù)你想要比較它們的方式,你最終可能會(huì)實(shí)現(xiàn)像__lt__()、__le__()、__gt__()、 __ge__() 或__eq__() 這樣的函數(shù),以便能夠使用相應(yīng)的<、<=、>、>=和==操作符。另一方面,你可以使用@total_ordering裝飾器。這樣,你只需要實(shí)現(xiàn)一個(gè)或多個(gè)豐富的比較排序方法,這個(gè)裝飾器就會(huì)為你提供其余的方法。此外,我也建議你定義 __eq__()方法。
假設(shè)你有一個(gè)Pythonista類,你希望能夠按字典順序?qū)λ鼈冞M(jìn)行排序。要做到這一點(diǎn),你需要實(shí)現(xiàn)豐富的比較排序方法。但是,我們并沒有實(shí)現(xiàn)所有這些方法,而是只實(shí)現(xiàn)了__lt__()方法和__eq__()方法。通過使用@ total_ordering修飾符,其他方法可以被自動(dòng)定義。

執(zhí)行該腳本將打印出True,因?yàn)閏在v之前。注意,盡管我們沒有顯式地實(shí)現(xiàn)__ge__(),但我們也可以使用>操作符。
如果希望根據(jù)不同的屬性比較實(shí)例,@total_ordering裝飾器是一種減少代碼行數(shù)和調(diào)整代碼的位置的好方法。但是,使用@total_ordering裝飾器會(huì)增加開銷,從而導(dǎo)致執(zhí)行速度變慢。此外,派生的比較方法的堆棧跟蹤更為復(fù)雜。因此,如果你需要非常高性能的代碼,你就不應(yīng)該使用該裝飾器,而應(yīng)該自己去實(shí)現(xiàn)所需的方法。
partial() - 簡化簽名
使用partial()你可以創(chuàng)建partial對象。這些對象的行為類似于傳遞給partial()的函數(shù),該函數(shù)使用提供給partial()的(關(guān)鍵字)參數(shù)進(jìn)行調(diào)用。因此,與原始函數(shù)相比,新創(chuàng)建的(partial)對象具有一個(gè)簡化的簽名。
這里是一個(gè)例子:

我們基于內(nèi)置的int()函數(shù)創(chuàng)建了一個(gè)partial對象。在本例中,我們提供base=2作為關(guān)鍵字參數(shù)。因此,新創(chuàng)建的basetwo對象的行為就像我們用base=2調(diào)用int()一樣。但是,我們?nèi)匀豢梢酝ㄟ^向base2提供一個(gè)base參數(shù)來覆蓋這種行為。因此,執(zhí)行basetwo("10010", base=10)計(jì)算的結(jié)果與int("10010")相同。
我們來看另一個(gè)例子。

這個(gè)函數(shù)會(huì)計(jì)算二維空間中兩點(diǎn)之間的歐氏距離。我們可以創(chuàng)建一個(gè)partial對象,它只接受一個(gè)參數(shù)(一個(gè)點(diǎn))并計(jì)算我們所提供的點(diǎn)與點(diǎn)(0,0)之間的歐式距離。
partialmethod() - 方法的partial()
partialmethod()是一個(gè)函數(shù),它會(huì)返回partialmethod描述符。你可以將它看作方法的partial()函數(shù)。這意味著它不是可調(diào)用的,而只是定義新方法的一種方式。我非常喜歡Python文檔[6]中的示例,所以我們來看看它。

我們定義一個(gè)表示單個(gè)單元格的類Cell。它有一個(gè)alive屬性和一個(gè)將alive設(shè)置為True或False的實(shí)例方法set_state()。此外,我們還創(chuàng)建了兩個(gè)partialmethod描述符set_alive()和set_dead(),它們會(huì)分別用True和False調(diào)用set_state()。這允許我們創(chuàng)建Cell類的一個(gè)新實(shí)例,調(diào)用set_alive()將該單元格的狀態(tài)更改為True并打印出該屬性的值。
reduce() - 基于多個(gè)值計(jì)算單個(gè)值
假設(shè)你有一個(gè)由數(shù)字組成的可迭代對象,并希望將其縮減為單個(gè)值。在本例中,結(jié)果值是所提供的可迭代對象的所有元素的和。實(shí)現(xiàn)此目的的一種方法是使用reduce()。

如你所見,我們定義了一個(gè)包含數(shù)字1到5的列表。我們通過以operator.add()作為第一個(gè)參數(shù),以該列表為第二個(gè)參數(shù)調(diào)用reduce()函數(shù)來計(jì)算這個(gè)列表中所有元素的和。當(dāng)然,你也可以使用內(nèi)置的sum()函數(shù),但是如果你想計(jì)算所有元素的乘積呢?你惟一需要更改的是將operator.add()函數(shù)替換為operator.mul() - 搞定!
@singledispatch - 函數(shù)重載
根據(jù)定義,@singledispatch裝飾器會(huì)將一個(gè)函數(shù)轉(zhuǎn)換為一個(gè)單分派泛函數(shù)。在@singledispatch的情況下,分派發(fā)生在第一個(gè)參數(shù)的類型上。
備注: 泛函數(shù)是由多個(gè)函數(shù)組成的函數(shù),這些函數(shù)為不同的類型實(shí)現(xiàn)了相同的操作。在調(diào)用期間應(yīng)該使用哪個(gè)實(shí)現(xiàn)由分派算法[7]決定。
備注: 單分派是泛函數(shù)分派的一種形式,其中,實(shí)現(xiàn)是基于單個(gè)參數(shù)[8]的類型進(jìn)行選擇的。
簡單來說,@singledispatch允許你在Python中重載函數(shù)。讓我們以一個(gè)例子來說明它。

在這個(gè)例子中,我們定義了一個(gè)函數(shù)mul(),它接受兩個(gè)參數(shù)并返回它們的乘積。然而,在Python中,兩個(gè)字符串相乘會(huì)引發(fā)一個(gè)TypeError。我們可以通過注冊_()函數(shù)來提供一個(gè)補(bǔ)丁。執(zhí)行腳本后的結(jié)果是:

@singledispatchmethod - 方法重載
@singledispatchmethod 裝飾器解決了與@singledispatch裝飾器相同的任務(wù),只不過它是針對方法的。

Negator類有一個(gè)名為neg()的實(shí)例方法。在默認(rèn)情況下,neg()函數(shù)會(huì)引發(fā)一個(gè)NotImplementedError。但是,對于整數(shù)和布爾類型,該函數(shù)會(huì)被重載,并在這些情況下返回否定。執(zhí)行腳本后的結(jié)果是:

update_wrapper() - 隱藏包裝器函數(shù)
update_wrapper()函數(shù)背后的思想是以一種方式更新一個(gè)包裝器函數(shù)(顧名思義),使其看起來像包裝后的函數(shù)。為了實(shí)現(xiàn)這一點(diǎn),update_wrapper()將包裝后的函數(shù)__module__, __name__, __qualname__, __annotations__和 __doc__賦給包裝器函數(shù)。此外,它還會(huì)更新該包裝器函數(shù)的__dict__。
讓我們以一個(gè)實(shí)際的例子看一下@wraps裝飾器。
@wraps - update_wrapper()的便捷函數(shù)
@wraps是一個(gè)裝飾器,它充當(dāng)一個(gè)調(diào)用update_wrapper()的便捷函數(shù)。確切地說,它與調(diào)用partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)是一樣的。在閱讀了關(guān)于update_wrapper()和@wraps的技術(shù)細(xì)節(jié)之后,你可能會(huì)問自己我們?yōu)槭裁葱枰[藏我們的包裝器函數(shù)。
下面的代碼片段定義了一個(gè)裝飾器@show_args。它會(huì)在函數(shù)自身被調(diào)用之前打印出用來調(diào)用該函數(shù)的參數(shù)和關(guān)鍵字參數(shù)。

現(xiàn)在,我們可以定義一個(gè)函數(shù)add(),它會(huì)返回兩個(gè)傳遞的整數(shù)的和。此外,我們還會(huì)將新編寫的裝飾器應(yīng)用于它,因?yàn)槲覀儗υ摵瘮?shù)的參數(shù)和關(guān)鍵字參數(shù)比較感興趣。在腳本的最后,我們打印了一個(gè)簡單加法的結(jié)果以及該函數(shù)的文檔字符串和名稱。

你是否期望看到一個(gè)與打印的文檔字符串和名稱不同的文檔字符串和名稱呢? 這是因?yàn)槲覀儧]有訪問包裝后的函數(shù)的文檔字符串和名稱,而是訪問了包裝器函數(shù)的文檔字符串和名稱。這里@wraps就派上用場了。我們需要在代碼中更改的惟一的東西就是將這個(gè)裝飾器應(yīng)用到wrapper()函數(shù)。

如果我們現(xiàn)在運(yùn)行該腳本,我們會(huì)看到預(yù)期的輸出:

總 結(jié)
恭喜,你已經(jīng)順利閱讀完了這篇文章!現(xiàn)在,你已經(jīng)對functools模塊所包含的函數(shù)有了大致的了解。此外,你還實(shí)現(xiàn)了一些示例,其中的這些函數(shù)非常有用。
希望你享受閱讀這篇文章。記得與你的朋友和同事分享哦。如果你還沒有,請考慮在Twitter上關(guān)注我,我是@DahlitzF,或者訂閱我的時(shí)事通訊,這樣你就不會(huì)錯(cuò)過以后的文章了。保持好奇心,持續(xù)編碼!
參考資料
functools文檔?
內(nèi)置property()函數(shù)
Python裝飾器入門
itemgetter()文檔
Python 中的記憶化:如何緩存函數(shù)結(jié)果
partialmethod()文檔 ?
泛函數(shù) - 詞條
單一分派 - 詞條
英文原文:https://florian-dahlitz.de/blog/introduction-to-functools
譯者:天天向
↓掃描關(guān)注本號↓
