簡約不簡單的匿名函數-day8

簡約不簡單的匿名函數

寫在前面

你好,我是禪墨!

昨天重學了Python 中的“常規”函數,用途十分廣泛。不過,除了常規函數,你應該也會在代碼中見到一些“非常規”函數,它們往往很簡短,就一行,並且有個很酷炫的名字——lambda,沒錯,這就是匿名函數。

匿名函數在實際工作中同樣舉足輕重,正確地運用匿名函數,能讓我們的代碼更簡潔、易讀。今天繼續 Python 的函數之旅,一起來學習這個簡約而不簡單的匿名函數。

匿名該函數基礎

首先,什麼是匿名函數呢?以下是匿名函數的格式:

lambda argument1, argument2,… argumentN : expression

我們可以看到,匿名函數的關鍵字是 lambda,之後是一系列的參數,然後用冒號隔開,最後則是由這些參數組成的表達式。我們通過幾個例子看一下它的用法:


square = lambda x: x**2
square(3)

9

這裏的匿名函數只輸入一個參數 x,輸出則是輸入 x 的平方。因此當輸入是 3 時,輸出便是 9。如果把這個匿名函數寫成常規函數的形式,則是下面這樣:


def square(x):
    return x**2
square(3)
 
9

可以看到,匿名函數 lambda 和常規函數一樣,返回的都是一個函數對象(function object),它們的用法也極其相似,不過還是有下面幾點區別。

第一,lambda 是一個表達式(expression),並不是一個語句(statement)。

  • 所謂的表達式,就是用一系列“公式”去表達一個東西,比如x + 2、 x**2等等;
  • 而所謂的語句,則一定是完成了某些功能,比如賦值語句x = 1完成了賦值,print 語句print(x)完成了打印,條件語句 if x < 0:完成了選擇功能等等。

因此,lambda 可以用在一些常規函數 def 不能用的地方,比如,lambda 可以用在列表內部,而常規函數卻不能:


[(lambda x: x*x)(x) for x in range(10)]
# 輸出
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

再比如,lambda 可以被用作某些函數的參數,而常規函數 def 也不能:


l = [(1, 20), (3, 0), (9, 10), (2, -1)]
l.sort(key=lambda x: x[1]) # 按列表中元組的第二個元素排序
print(l)
# 輸出
[(2, -1), (3, 0), (9, 10), (1, 20)]

常規函數 def 必須通過其函數名被調用,因此必須首先被定義。但是作爲一個表達式的 lambda,返回的函數對象就不需要名字了。

第二,lambda 的主體是隻有一行的簡單表達式,並不能擴展成一個多行的代碼塊。

這其實是出於設計的考慮。Python 之所以發明 lambda,就是爲了讓它和常規函數各司其職:lambda 專注於簡單的任務,而常規函數則負責更復雜的多行邏輯。關於這點,Python 之父 Guido van Rossum 曾發了一篇 文章解釋,你有興趣的話可以自己閱讀。

爲什麼要使用匿名函數?

理論上來說,Python 中有匿名函數的地方,都可以被替換成等價的其他表達形式。一個 Python 程序是可以不用任何匿名函數的。不過,在一些情況下,使用匿名函數 lambda,可以幫助我們大大簡化代碼的複雜度,提高代碼的可讀性。

通常,我們用函數的目的無非是這麼幾點:

  1. 減少代碼的重複性;
  2. 模塊化代碼。

對於第一點,如果你的程序在不同地方包含了相同的代碼,那麼我們就會把這部分相同的代碼寫成一個函數,併爲它取一個名字,方便在相對應的不同地方調用。

對於第二點,如果你的一塊兒代碼是爲了實現一個功能,但內容非常多,寫在一起降低了代碼的可讀性,那麼通常我們也會把這部分代碼單獨寫成一個函數,然後加以調用。

不過,再試想一下這樣的情況。你需要一個函數,但它非常簡短,只需要一行就能完成;同時它在程序中只被調用一次而已。那麼請問,你還需要像常規函數一樣,給它一個定義和名字嗎?

答案當然是否定的。這種情況下,函數就可以是匿名的,你只需要在適當的地方定義並使用,就能讓匿名函數發揮作用了。

舉個例子,如果你想對一個列表中的所有元素做平方操作,而這個操作在你的程序中只需要進行一次,用 lambda 函數可以表示成下面這樣:

squared = map(lambda x: x**2, [1, 2, 3, 4, 5])

如果用常規函數,則表示爲這幾行代碼:


from tkinter import Button, mainloop
button = Button(
    text='This is a button',
    command=lambda: print('being pressed')) # 點擊時調用lambda函數
button.pack()
mainloop()

顯然,運用匿名函數的代碼簡潔很多,也更加符合 Python 的編程習慣。

Python 函數式編程

最後,我們一起來看一下,Python 的函數式編程特性,這與我們今天所講的匿名函數 lambda,有着密切的聯繫。

所謂函數式編程,是指代碼中每一塊都是不可變的(immutable),都由純函數(pure function)的形式組成。這裏的純函數,是指函數本身相互獨立、互不影響,對於相同的輸入,總會有相同的輸出,沒有任何副作用。

舉個很簡單的例子,比如對於一個列表,我想讓列表中的元素值都變爲原來的兩倍,我們可以寫成下面的形式:


def multiply_2(l):
    for index in range(0, len(l)):
        l[index] *= 2
    return l

這段代碼就不是一個純函數的形式,因爲列表中元素的值被改變了,如果我多次調用 multiply_2() 這個函數,那麼每次得到的結果都不一樣。要想讓它成爲一個純函數的形式,就得寫成下面這種形式,重新創建一個新的列表並返回。


def multiply_2_pure(l):
    new_list = []
    for item in l:
        new_list.append(item * 2)
    return new_list

函數式編程的優點,主要在於其純函數和不可變的特性使程序更加健壯,易於調試(debug)和測試;缺點主要在於限制多,難寫。當然,Python 不同於一些語言(比如 Scala),它並不是一門函數式編程語言,不過,Python 也提供了一些函數式編程的特性,值得我們瞭解和學習。

Python 主要提供了這麼幾個函數:map()、filter() 和 reduce(),通常結合匿名函數 lambda 一起使用。這些都是你需要掌握的東西,接下來我逐一介紹。

首先是 map(function, iterable) 函數,前面的例子提到過,它表示,對 iterable 中的每個元素,都運用 function 這個函數,最後返回一個新的可遍歷的集合。比如剛纔列表的例子,要對列表中的每個元素乘以 2,那麼用 map 就可以表示爲下面這樣:


l = [1, 2, 3, 4, 5]
new_list = map(lambda x: x * 2, l) # [2, 4, 6, 8, 10]

我們可以以 map() 函數爲例,看一下 Python 提供的函數式編程接口的性能。還是同樣的列表例子,它還可以用 for 循環和 list comprehension(目前沒有統一中文叫法,你也可以直譯爲列表理解等)實現,我們來比較一下它們的速度:


python3 -mtimeit -s'xs=range(1000000)' 'map(lambda x: x*2, xs)'
2000000 loops, best of 5: 171 nsec per loop

python3 -mtimeit -s'xs=range(1000000)' '[x * 2 for x in xs]'
5 loops, best of 5: 62.9 msec per loop

python3 -mtimeit -s'xs=range(1000000)' 'l = []' 'for i in xs: l.append(i * 2)'
5 loops, best of 5: 92.7 msec per loop

你可以看到,map() 是最快的。因爲 map() 函數直接由 C 語言寫的,運行時不需要通過 Python 解釋器間接調用,並且內部做了諸多優化,所以運行速度最快。

接下來來看 filter(function, iterable) 函數,它和 map 函數類似,function 同樣表示一個函數對象。filter() 函數表示對 iterable 中的每個元素,都使用 function 判斷,並返回 True 或者 False,最後將返回 True 的元素組成一個新的可遍歷的集合。

舉個例子,比如我要返回一個列表中的所有偶數,可以寫成下面這樣:


l = [1, 2, 3, 4, 5]
new_list = filter(lambda x: x % 2 == 0, l) # [2, 4]

最後我們來看 reduce(function, iterable) 函數,它通常用來對一個集合做一些累積操作。

function 同樣是一個函數對象,規定它有兩個參數,表示對 iterable 中的每個元素以及上一次調用後的結果,運用 function 進行計算,所以最後返回的是一個單獨的數值。

舉個例子,我想要計算某個列表元素的乘積,就可以用 reduce() 函數來表示:


l = [1, 2, 3, 4, 5]
product = reduce(lambda x, y: x * y, l) # 1*2*3*4*5 = 120

當然,類似的,filter() 和 reduce() 的功能,也可以用 for 循環或者 list comprehension 來實現。

通常來說,在我們想對集合中的元素進行一些操作時,如果操作非常簡單,比如相加、累積這種,那麼我們優先考慮 map()、filter()、reduce() 這類或者 list comprehension 的形式。至於這兩種方式的選擇:

  • 在數據量非常多的情況下,比如機器學習的應用,那我們一般更傾向於函數式編程的表示,因爲效率更高;
  • 在數據量不多的情況下,並且你想要程序更加 Pythonic 的話,那麼 list comprehension 也不失爲一個好選擇。

不過,如果你要對集合中的元素,做一些比較複雜的操作,那麼,考慮到代碼的可讀性,我們通常會使用 for 循環,這樣更加清晰明瞭。

總結

今天,我們一起學習了 Python 中的匿名函數 lambda,它的主要用途是減少代碼的複雜度。需要注意的是 lambda 是一個表達式,並不是一個語句;它只能寫成一行的表達形式,語法上並不支持多行。匿名函數通常的使用場景是:程序中需要使用一個函數完成一個簡單的功能,並且該函數只調用一次。

寫在後面

今天就是第八天了!
不出意外的話基礎篇剩三篇就結束了!
下一階段就是高階篇哦,歡迎給建議啊!

有了解華碩天選的嗎?香嗎?歡迎評論!

CSDN:禪墨雲

知乎:禪墨雲

微信公衆號:興趣路人甲

這裏是引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章