Map 函數(shù)的隊友與對手
本文將學(xué)習(xí)如何將 map() 與其他函數(shù)工具結(jié)合起來,并進行更復(fù)雜的轉(zhuǎn)換;并學(xué)習(xí)可以用什么工具來替代map(),使代碼更加Pythonic。
map() 與其他函數(shù)結(jié)合使用
現(xiàn)在我們已經(jīng)介紹了如何使用 map() 來完成涉及迭代表的不同任務(wù)。然而,如果將map()?與其他函數(shù)式工具(如filter()? 和 reduce())一起使用,那么我們可以對迭代變量進行更復(fù)雜的轉(zhuǎn)換。這就是在下面要介紹的內(nèi)容。
map() 和 filter()
有時需要處理一個輸入迭代器,并返回另一個迭代器,這個迭代器是過濾掉輸入迭代器中不需要的值而得到的。在這種情況下,Python 的 filter()? 可以是一個很好的選擇。filter() 是一個內(nèi)置的函數(shù),需要兩個位置參數(shù)。
- function 函數(shù)將是一個謂詞或布爾值函數(shù),一個根據(jù)輸入數(shù)據(jù)返回真或假的函數(shù)。
- iterable 是任何Python可迭代對象。
filter()? 產(chǎn)生的是函數(shù)返回 True 的輸入迭代器的項目。如果把 None傳給函數(shù),那么 filter()? 使用身份函數(shù)。這意味著 filter() 將檢查可迭代對象中每個項目的真值,并過濾掉所有虛假的項目。
為了說明如何使用 map()? 和 filter(),假設(shè)我們需要計算一個列表中所有數(shù)值的平方根,如果列表中包含負值時,會得到一個錯誤。
如果有一個負數(shù)作為參數(shù),math.sqrt()?會引發(fā)一個ValueError。為了避免這個問題,可以使用filter()來過濾掉所有的負值,然后找到剩余正值的平方根。看下面的例子。
is_positive()? 是一個謂詞函數(shù),它接受一個數(shù)字作為參數(shù),如果該數(shù)字大于或等于0,則返回True??梢詫?nbsp;is_positive()? 傳遞給 filter()?,以去除數(shù)字中的所有負數(shù)。因此對 map()? 的調(diào)用將只處理正數(shù),從而確保 math.sqrt() 不會拋出一個ValueError。
map() 和 reduce()
reduce()? 是一個函數(shù),它存在于 Python 標準庫中一個叫做 functools? 的模塊中。reduce() ?是 Python 的另一個核心函數(shù)工具,當我們需要將一個函數(shù)應(yīng)用于一個迭代器并將其減少到一個單一的累積值時,它非常有用,這種操作通常被稱為減少或折疊。reduce() 需要兩個必要的參數(shù)。
- function 函數(shù)可以是任何接受兩個參數(shù)并返回一個值的 Python 可調(diào)用函數(shù)。
- iterable 可以是任何 Python 的可迭代對象。
reduce() 將對可迭代對象 iterable 中的所有項目應(yīng)用函數(shù),并累積計算出一個最終值。
下面的例子結(jié)合了 map()? 和 reduce() 來計算主目錄中所有文件的總大小:
在這個例子中,我們調(diào)用os.path.expanduser("~")?來獲得主目錄的路徑。然后在該路徑上調(diào)用 os.listdir() ,得到一個包含所有文件路徑的列表。
對map()?的調(diào)用使用 os.path.getsize()? 來獲得每個文件的大小。最后使用 reduce()? 和operator.add() 來獲得每個文件大小的累積總和。最后的結(jié)果是主目錄中所有文件的總大小,單位是字節(jié)。
注意:幾年前,谷歌開發(fā)并開始使用他們稱之為MapReduce的編程模型。這是一種新的數(shù)據(jù)處理方式,旨在使用集群上的并行和分布式計算來管理大數(shù)據(jù)。
這個模型的靈感來自于函數(shù)式編程中常用的map和reduce操作的結(jié)合。
MapReduce模型對谷歌在合理時間內(nèi)處理海量數(shù)據(jù)的能力產(chǎn)生了巨大影響。然而,到了2014年,谷歌不再使用MapReduce作為他們的主要處理模式。
現(xiàn)在,我們可以找到一些MapReduce的替代實現(xiàn),如 Apache Hadoop,它是一個使用MapReduce模型的開源軟件工具的集合。
盡管可以使用 reduce()? 來解決本節(jié)所涉及的問題,但 Python 提供了其他的工具,這些工具可以導(dǎo)致一個更加 Pythonic 和高效的解決方案。例如可以使用內(nèi)置的函數(shù) sum() 來計算主目錄中的文件的總大小。
這個例子比我們之前看到的例子可讀性和效率都要高很多。
用 starmap() 處理基于元組的可迭代對象
Python的 itertools.starmap() 生成一個迭代器,該迭代器將函數(shù)應(yīng)用于從元組可迭代對象獲得的參數(shù),并產(chǎn)生結(jié)果。當處理已經(jīng)分組在元組中的可迭代對象時,它很有用。
map()? 和 starmap()? 之間的主要區(qū)別在于后者使用解包操作符( *? )調(diào)用其轉(zhuǎn)換函數(shù),將每個元組參數(shù)解包為幾個位置參數(shù)。因此,轉(zhuǎn)換函數(shù)被稱為 function(*args)? 而不是function(arg1, arg2,... argN)。
starmap()的官方文檔[1]說,該函數(shù)大致等同于下面的Python函數(shù)。
這個函數(shù)中的for循環(huán)對iterable中的項目進行迭代,并產(chǎn)生轉(zhuǎn)換后的項目作為結(jié)果。對function(*args)的調(diào)用使用了解包操作符,將圖元解包為幾個位置參數(shù)。下面是一些關(guān)于starmap()如何工作的例子。
這個函數(shù)中的for? 循環(huán)遍歷iterable?中的元素,并得到轉(zhuǎn)換后的結(jié)果。調(diào)用function(*args)?使用解包操作符將元組解包為幾個位置參數(shù)。下面是一些關(guān)于starmap()如何工作的例子:
在第一個例子中,使用pow()?來計算每個元組中第一個值對第二個值的升冪。這些元組的形式是(基數(shù), 指數(shù))。
如果可迭代對象中的每個元組都有兩個元素,那么 function?也必須接受兩個參數(shù)。如果元組有三個元素,那么 function?必須接受三個參數(shù),依此類推。否則會得到一個TypeError
如果使用 map()? 而不是 starmap()?,那么會得到一個不同的結(jié)果,因為 map() 從每個元組中抽取一個項目。
注意,map()? 需要兩個元組,而不是一個元組的列表。map()? 在每次迭代中也從每個元組中獲取一個值。要使 map()? 返回與 starmap() 相同的結(jié)果,需要交換值。
在這種情況下,我們有兩個元組而不是一個元組的列表,還交換了7和4。現(xiàn)在,第一個元組提供基數(shù),第二個元組提供指數(shù)。
用Pythonic風(fēng)格編碼取代map()
像 map()、filter()? 和 reduce() 這樣的函數(shù)式編程工具已經(jīng)存在很長時間了。然而,列表推導(dǎo)式和生成器表達式幾乎在每個用例中都成為了它們的自然替代品。
例如,map() 提供的功能幾乎總是用一個列表推導(dǎo)式或生成器表達式來表達更好。在下面兩節(jié)中,我們將學(xué)習(xí)如何用列表推導(dǎo)式或生成器表達式來替換對map()的調(diào)用,使我們的代碼更有可讀性和Pythonic。
使用列表推導(dǎo)式
有一個一般的模式,我們可以用一個列表推導(dǎo)式來代替對map()的調(diào)用。具體方法如下。
注意,列表推導(dǎo)幾乎總是比調(diào)用map()?讀起來更清楚。由于列表推導(dǎo)式在Python開發(fā)人員中非常流行,所以在任何地方都可以找到它們。因此,用列表推導(dǎo)式替換 map() 調(diào)用會讓其他Python開發(fā)人員更熟悉你的代碼。
這里有一個例子,說明如何用一個列表推導(dǎo)式來代替map(),建立一個平方數(shù)的列表。
如果我們比較這兩種解決方案,那么我們可能會說,使用列表理解的那個方案更有可讀性,因為它讀起來幾乎像純英語。另外,列表理解避免了在map()上明確調(diào)用list()來建立最終的列表。
使用生成器表達式
map()返回一個map對象,它是一個迭代器,可以按需產(chǎn)生項目。因此,map()的自然替代物是一個生成器表達式[2],因為生成器表達式返回生成器對象,而生成器對象也是按需產(chǎn)生項目的迭代器。
map()?返回一個map對象,這是一個按需生成項目的迭代器。因此對map()的自然替換是一個生成器表達式,因為生成器表達式返回生成器對象,這些對象也是生成按需項的迭代器。
眾所周知,Python迭代器在內(nèi)存消耗方面是非常高效的。這就是為什么map()現(xiàn)在返回一個迭代器而不是一個列表的原因。
列表推導(dǎo)式和生成器表達式之間有一個微小的語法差異。第一種方法使用一對方括號('[]'?)來分隔表達式。第二個使用一對圓括號('()')。因此,要將列表推導(dǎo)式轉(zhuǎn)換為生成器表達式,只需將方括號替換為圓括號。
可以使用生成器表達式來編寫代碼,它比使用map()的代碼讀起來更清晰。請看下面的例子。
這段代碼與上一節(jié)的代碼有一個主要區(qū)別:把方括號改為一對小括號,把列表理解變成生成器表達式。
生成器表達式通常用作函數(shù)調(diào)用中的參數(shù)。在這種情況下,不需要使用圓括號來創(chuàng)建生成器表達式,因為用于調(diào)用函數(shù)的圓括號還提供了構(gòu)建生成器的語法。有了這個想法,通過像這樣調(diào)用 list(),你可以得到與上面例子相同的結(jié)果:
如果在函數(shù)調(diào)用中使用生成器表達式作為參數(shù),那么就不需要額外的一對小括號。我們用來調(diào)用函數(shù)的小括號提供了構(gòu)建生成器的語法。
在內(nèi)存消耗方面,生成器表達式與 map() 一樣高效,因為它們都返回按需生成項的迭代器。然而,生成器表達式幾乎總是會提高代碼的可讀性。它們還使您的代碼在其他Python開發(fā)人員眼中更像Python。
總結(jié)
我們可以使用Python的 map()? 對可迭代對象執(zhí)行映射操作。映射操作包括對可迭代對象中的元素應(yīng)用轉(zhuǎn)換函數(shù)來生成轉(zhuǎn)換后的可迭代對象。通常,map()可以處理和轉(zhuǎn)換可迭代對象,而無需使用顯式循環(huán)。
在本文中,我們已經(jīng)學(xué)習(xí)了map()?如何工作以及如何使用它來處理可迭代對象。還了解了一些可以在代碼中替換map()的python工具。
至此我們現(xiàn)在知道如何:
- 使用Python的map()
- 使用map()來處理和轉(zhuǎn)換可迭代對象,而不使用顯式循環(huán)
- 將map()? 與 filter()? 和 reduce()等函數(shù)組合在一起,以執(zhí)行復(fù)雜的轉(zhuǎn)換
- 用列表推導(dǎo)式和生成器表達式等工具替換 map()
有了這些新知識,將能夠在代碼中使用map()?,并以函數(shù)式編程風(fēng)格處理代碼。通過將map()替換為列表推導(dǎo)式或生成器表達式,還可以切換到更python化和更現(xiàn)代的風(fēng)格。
參考資料
[1]starmap的官方文檔: https://docs.python.org/3/library/itertools.html#itertools.starmap
[2]生成器表達式: https://realpython.com/introduction-to-python-generators/#building-generators-with-generator-expressions