1. 程式人生 > >Python專題——五分鐘帶你瞭解map、reduce和filter

Python專題——五分鐘帶你瞭解map、reduce和filter

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注

今天是Python專題第6篇文章,給大家介紹的是Python當中三個非常神奇的方法:map、reduce和filter。

不知道大家看到map和reduce的時候有沒有什麼感覺,如果看過之前我們大資料系列介紹MapReduce文章的同學,想必有些印象。這個MapReduce不是一個分散式的計算方法麼,怎麼又變成Python中的方法了?其實原因很簡單,因為Python是一門很年輕的語言,它在發展的過程當中吸收了很多其他領域的精華,MapReduce就是其中之一。

對之前文章感興趣的同學可以點選下方的連結,回顧一下之前MapReduce的內容。

[大資料基石——Hadoop與MapReduce](https://mp.weixin.qq.com/s?__biz=MzUyMTM5OTM2NA==&mid=2247483739&idx=1&sn=2111080662444c8e4f7bf10448e6d734&chksm=f9dafc70cead7566d54126ed9d78069f313132e3c094946303f6c820e39a49c3bb794dce3921&scene=21#wechat_redirect)

map

map除了地圖之外,另一個英文字意是對映。在C++和Java一些語言當中,將map進一步引申成了儲存key和value對映結構的容器。Python對這點做了區分,KV結構的容器命名成了dict,即字典,而map則回到了它的本意,也就是對映。

我們都知道,在數學領域,對映也是函式的定義。一個自變數通過某種對映,對應到一個因變數。同樣,在Python當中,map操作本質也是函式,不過它作用的範圍不再是單個變數,而是一個序列。換句話說,通過map我們可以省去迴圈操作,可以自動將一個容器當中的元素套用一個函式。

舉個簡單的例子,比如我們有一個座標,我們希望知道它距離原點的距離。這個問題很簡單,我們寫一個計算距離的函式就可以解決:

def dis(point):
return math.sqrt(point[0]**2 + point[1]**2)

那如果我有多個點需要計算距離,在map出現之前,我們只能用迴圈來解決問題:

points = [[0, 1], [2, 4], [3, 2]]

for point in points:
print(dis(point))

但是有了map之後, 我們可以省去迴圈的操作,整個程式碼簡化成了一行:

map(dis, points)

但是要注意,我們呼叫完map之後得到的結果不是一個list而是一個迭代器。我們直接將map返回的內容print出來,可以得到這樣一個結果:

>>> print(map(dis, points))
<map object at 0x107aad1d0>

這是一個類的標準輸出,其實它返回的不是最後的結果,而是一個迭代器。我們在之前的文章當中已經介紹過了迭代器和生成器的相關概念,這裡不多做贅述了,遺忘的同學可以點選下方連結回顧一下之前的內容:

[Python——五分鐘帶你弄懂迭代器與生成器](https://mp.weixin.qq.com/s?__biz=MzUyMTM5OTM2NA==&mid=2247484777&idx=1&sn=236b215aff90bf4cccb56b8385eece18&chksm=f9daf842cead7154ad10951904789ff549611e86df4fefae71653c7c96be627bee447dfc7f89&scene=21#wechat_redirect)

我們想要獲得完整的內容也很容易,我們只需要將它轉化成list型別即可:

>>> print(list(map(dis, points)))
[1.0, 4.47213595499958, 3.605551275463989]

以上過程還可以進一步簡化,還記得我們之前介紹過的匿名函式嗎?由於dis函式在我們的程式當中只會在map中用到,我們完全沒有必要單獨建立一個函式,我們可以直接傳入一個匿名函式搞定運算:

map(lambda x: math.sqrt(x[0]**2 + x[1] ** 2), points)

簡單總結一下,map操作其實執行的是一個對映。它可以自動地將一個序列當中的內容通過制定的函式對映成另一個序列,從而避免顯式地使用迴圈來呼叫,在很多場景下可以大大地簡化程式碼的編寫,可以很方便地將一個序列整體轉變成另一個結果。

reduce

相比於map,reduce的操作稍稍難理解一點點。它也是規定一個對映,不過不是將一個元素對映成一個結果。而是將兩個元素歸併成一個結果。並且它並不是呼叫一次,而是依次呼叫,直到最後只剩下一個結果為止。

比如說我們有一個數組[a, b, c, d]和一個函式f,我們計算reduce(f, [a, b, c, d])其實就等價於f(f(f(a, b), c), d)。和map不同的是,reduce最後得到一個結果,而不是一個迭代器或者是list。

我們光說有些抽象,不妨來看一個例子,就看最簡單的一個例子:reduce函式接收兩個數,返回兩個數的和。那麼顯然,我們依次呼叫reduce,得到的就是原陣列的和。

from functools import reduce

def f(a, b):
return a + b

print(reduce(f, [1, 2, 3, 4]))

最終得到的結果當然是10,同樣,我們也可以將reduce中的方法定義成匿名函式,一樣不影響最終的結果。

print(reduce(lambda x, y: x + y, [1, 2, 3, 4]))

MapReduce

既然我們map和reduce都有了,顯然我們可以將它們串聯起來使用,也就是分散式系統當中MapReduce的做法。雖然如果不手動使用執行緒池的話,Python並不會起多個執行緒來加速運算,但是至少可以簡化我們實現的程式碼。我們還是舉經典的wordCount的例子,也就是文字計算詞頻。

套用map和reduce的功能,整個流程非常清晰,我們只需要在map階段對文字進行分詞,在reduce階段對分詞之後的結果進行彙總即可。

聽著好像非常容易,但是你實際去上手是寫不出來的。原因也很簡單,因為hadoop當中的Map和Reduce中間還有一層shuffle的操作,會自動地將key值相同的結果放到同一個reducer當中。在這個問題當中,key自然就是我們的word,由於相同的word被放到同一個reducer當中,我們只需要累加就行了。但是如果我們自己編寫mapreduce的話,由於缺少了中間資料重排的步驟,所以導致不能實現。

要解決也簡單,我們可以人為增加一個map階段代替hadoop當中的重排。相當於做了一個MapMapReduce,我們來看程式碼:

from collections import Counter, defaultdict

texts = ['apple bear peach grape', 'grape orange pear']

# 第一次map,將字串轉成陣列,每個單詞對應1
def mp1(text):
ret = []
words = text.split(' ')
for word in words:
ret.append((word, 1))
return ret


# 第二次map,將陣列轉成dict
def mp2(arr):
d = defaultdict(int)
for k, v in arr:
d[k] += v
return d

# reduce,合併dict
def rd(x, y):
x.update(y)
return x

print(reduce(rd, map(mp2, map(mp1, texts))))

那如果我們不用多次MapReduce呢?也不是沒有辦法,需要取點巧,方法也簡單隻要使用之前我們講解過的Counter類,就可以完美解決這個問題。我們來看程式碼:

from collections import Counter

texts = ['apple bear peach grape', 'grape orange pear']

def mp(text):
words = text.split(' ')
return Counter(words)

print(reduce(lambda x, y: x + y, map(mp, texts)))

由於我們使用了Counter,所以我們在map階段返回的結果就已經是詞頻的dict了,而在reduce階段我們只需要將它們全部累加起來就OK了。

最後,我們來看下filter。

filter

filter的英文是過濾,所以它的使用就很明顯了。它的用法和map有些類似,我們編寫一個函式來判斷元素是否合法。通過呼叫filter,會自動將這個函式應用到容器當中所有的元素上,最後只會保留執行結果是True的元素,而過濾掉那些是False的元素。

舉個例子,假設我們想要保留list當中的奇數而過濾掉偶數,我們當然可以直接操作,比如:

arr = [1, 3, 2, 4, 5, 8]

[i for i in arr if i % 2 > 0 ]

而使用filter會非常方便:

list(filter(lambda x: x % 2 > 0, arr))

從這個例子當中可能看不出便捷,但是有的時候判斷的條件可能非常複雜,我們判斷的邏輯不能簡單地在list定義當中表達出來,這個時候使用filter則會容易得多。

最後, 我們再看一個類似的用法。在itertools當中有一個方法叫做 compress,通過compress我們可以實現根據一個序列的條件過濾另一個序列。

舉個簡單的例子,假設,我們有兩個陣列:

student = ['xiaoming', 'xiaohong', 'xiaoli', 'emily']
scores = [60, 70, 80, 40]

我們想要獲取所有考試及格的同學的list,如果用常規做法基本上免不了使用迴圈,但是使用compress可以很方便地通過一行程式碼實現:

from itemtools import compress

>>> pass = [i > 60 for i in scores]
>>> print(pass)
[False, True, True, False]

>>> list(compress(student, pass))
['xiaohong', 'xiaoli']

需要注意的是filter和compress返回的都是一個迭代器,我們要獲取它們的值,需要手動轉換成list。

雖然在日常的開發當中不使用這三樣神器同樣可以工作,但是用上它們之後,會提升很多程式碼的可讀性,節省很多無用的程式碼。尤其是在面試的時候,很有可能就會給面試官留下不一樣的印象,也許結果也會不同。

今天的文章就是這些,如果覺得有所收穫,請順手點個關注或者轉發吧,你們的舉手之勞對我來說很重要。

![](https://user-gold-cdn.xitu.io/2020/3/17/170e5e2cecdeb557?w=258&h=258&f=png&