這篇文章主要介紹了Python函數式編程指南(二):從函數開始,本文講解了定義一個函數、使用函數賦值、閉包、作為參數等內容,需要的朋友可以參考下
2. 從函數開始
2.1. 定義一個函數
如下定義了一個求和函數:
代碼如下:
def add(x, y):
return x + y
關於參數和返回值的語法細節可以參考其他文檔,這裡就略過了。
使用lambda可以定義簡單的單行匿名函數。lambda的語法是:
代碼如下:
lambda args: expression
參數(args)的語法與普通函數一樣,同時表達式(expression)的值就是匿名函數調用的返回值;而lambda表達式返回這個匿名函數。如果我們給匿名函數取個名字,就像這樣:
代碼如下:
lambda_add = lambda x, y: x + y
這與使用def定義的求和函數完全一樣,可以使用lambda_add作為函數名進行調用。然而,提供lambda的目的是為了編寫偶爾為之的、簡單的、可預見不會被修改的匿名函數。這種風格雖然看起來很酷,但並不是一個好主意,特別是當某一天需要對它進行擴充,再也無法用一個表達式寫完時。如果一開始就需要給函數命名,應該始終使用def關鍵字。
2.2. 使用函數賦值
事實上你已經見過了,上一節中我們將lambda表達式賦值給了add。同樣,使用def定義的函數也可以賦值,相當於為函數取了一個別名,並且可以使用這個別名調用函數:
代碼如下:
add_a_number_to_another_one_by_using_plus_operator = add
print add_a_number_to_another_one_by_using_plus_operator(1, 2)
既然函數可以被變量引用,那麼將函數作為參數和返回值就是很尋常的做法了。
2.3. 閉包
閉包是一類特殊的函數。如果一個函數定義在另一個函數的作用域中,並且函數中引用了外部函數的局部變量,那麼這個函數就是一個閉包。下面的代碼定義了一個閉包:
代碼如下:
def f():
n = 1
def inner():
print n
inner()
n = 'x'
inner()
函數inner定義在f的作用域中,並且在inner中使用了f中的局部變量n,這就構成了一個閉包。閉包綁定了外部的變量,所以調用函數f的結果是打印1和'x'。這類似於普通的模塊函數和模塊中定義的全局變量的關系:修改外部變量能影響內部作用域中的值,而在內部作用域中定義同名變量則將遮蔽(隱藏)外部變量。
如果需要在函數中修改全局變量,可以使用關鍵字global修飾變量名。Python 2.x中沒有關鍵字為在閉包中修改外部變量提供支持,在3.x中,關鍵字nonlocal可以做到這一點:
代碼如下:
#Python 3.x supports `nonlocal'
def f():
n = 1
def inner():
nonlocal n
n = 'x'
print(n)
inner()
print(n)
調用這個函數的結果是打印1和'x',如果你有一個Python 3.x的解釋器,可以試著運行一下。
由於使用了函數體外定義的變量,看起來閉包似乎違反了函數式風格的規則即不依賴外部狀態。但是由於閉包綁定的是外部函數的局部變量,而一旦離開外部函數作用域,這些局部變量將無法再從外部訪問;另外閉包還有一個重要的特性,每次執行至閉包定義處時都會構造一個新的閉包,這個特性使得舊的閉包綁定的變量不會隨第二次調用外部函數而更改。所以閉包實際上不會被外部狀態影響,完全符合函數式風格的要求。(這裡有一個特例,Python 3.x中,如果同一個作用域中定義了兩個閉包,由於可以修改外部變量,他們可以相互影響。)
雖然閉包只有在作為參數和返回值時才能發揮它的真正威力,但閉包的支持仍然大大提升了生產率。
2.4. 作為參數
如果你對OOP的模板方法模式很熟悉,相信你能很快速地學會將函數當作參數傳遞。兩者大體是一致的,只是在這裡,我們傳遞的是函數本身而不再是實現了某個接口的對象。
我們先來給前面定義的求和函數add熱熱身:
代碼如下:
print add('三角形的樹', '北極')
與加法運算符不同,你一定很驚訝於答案是'三角函數'。這是一個內置的彩蛋...bazinga!
言歸正傳。我們的客戶有一個從0到4的列表:
代碼如下:
lst = range(5) #[0, 1, 2, 3, 4]
雖然我們在上一小節裡給了他一個加法器,但現在他仍然在為如何計算這個列表所有元素的和而苦惱。當然,對我們而言這個任務輕松極了:
代碼如下:
amount = 0
for num in lst:
amount = add(amount, num)
這是一段典型的指令式風格的代碼,一點問題都沒有,肯定可以得到正確的結果。現在,讓我們試著用函數式的風格重構一下。
首先可以預見的是求和這個動作是非常常見的,如果我們把這個動作抽象成一個單獨的函數,以後需要對另一個列表求和時,就不必再寫一遍這個套路了:
代碼如下:
def sum_(lst):
amount = 0
for num in lst:
amount = add(amount, num)
return amount
print sum_(lst)
還能繼續。sum_函數定義了這樣一種流程:
1. 使用初始值與列表的第一個元素相加;
2. 使用上一次相加的結果與列表的下一個元素相加;
3. 重復第二步,直到列表中沒有更多元素;
4. 將最後一次相加的結果返回。
如果現在需要求乘積,我們可以寫出類似的流程——只需要把相加換成相乘就可以了:
代碼如下:
def multiply(lst):
product = 1
for num in lst:
product = product * num
return product
除了初始值換成了1以及函數add換成了乘法運算符,其他的代碼全部都是冗余的。我們為什麼不把這個流程抽象出來,而將加法、乘法或者其他的函數作為參數傳入呢?
代碼如下:
def reduce_(function, lst, initial):
result = initial
for num in lst:
result = function(result, num)
return result
print reduce_(add, lst, 0)
現在,想要算出乘積,可以這樣做:
代碼如下:
print reduce_(lambda x, y: x * y, lst, 1)
那麼,如果想要利用reduce_找出列表中的最大值,應該怎麼做呢?請自行思考:)
雖然有模板方法這樣的設計模式,但那樣的復雜度往往使人們更情願到處編寫循環。將函數作為參數完全避開了模板方法的復雜度。
Python有一個內建函數reduce,完整實現並擴展了reduce_的功能。本文稍後的部分包含了有用的內建函數的介紹。請注意我們的目的是沒有循環,使用函數替代循環是函數式風格區別於指令式風格的最顯而易見的特征。
*像Python這樣構建於類C語言之上的函數式語言,由於語言本身提供了編寫循環代碼的能力,內置函數雖然提供函數式編程的接口,但一般在內部還是使用循環實現的。同樣的,如果發現內建函數無法滿足你的循環需求,不妨也封裝它,並提供一個接口。
2.5. 作為返回值
將函數返回通常需要與閉包一起使用(即返回一個閉包)才能發揮威力。我們先看一個函數的定義:
代碼如下:
def map_(function, lst):
result = []
for item in lst:
result.append(function(item))
return result
函數map_封裝了最常見的一種迭代:對列表中的每個元素調用一個函數。map_需要一個函數參數,並將每次調用的結果保存在一個列表中返回。這是指令式的做法,當你知道了列表解析(list comprehension)後,會有更好的實現。
這裡我們先略過map_的蹩腳實現而只關注它的功能。對於上一節中的lst,你可能發現最後求乘積結果始終是0,因為lst中包含了0。為了讓結果看起來足夠大,我們來使用map_為lst中的每個元素加1:
代碼如下:
lst = map_(lambda x: add(1, x), lst)
print reduce_(lambda x, y: x * y, lst, 1)
答案是120,這還遠遠不夠大。再來:
代碼如下:
lst = map_(lambda x: add(10, x), lst)
print reduce_(lambda x, y: x * y, lst, 1)
囧,事實上我真的沒有想到答案會是360360,我發誓沒有收周鴻祎任何好處。
現在回頭看看我們寫的兩個lambda表達式:相似度超過90%,絕對可以使用抄襲來形容。而問題不在於抄襲,在於多寫了很多字符有木有?如果有一個函數,根據你指定的左操作數,能生成一個加法函數,用起來就像這樣:
代碼如下:
lst = map_(add_to(10), lst) #add_to(10)返回一個函數,這個函數接受一個參數並加上10後返回
寫起來應該會舒服不少。下面是函數add_to的實現:
代碼如下:
def add_to(n):
return lambda x: add(n, x)
通過為已經存在的某個函數指定數個參數,生成一個新的函數,這個函數只需要傳入剩余未指定的參數就能實現原函數的全部功能,這被稱為偏函數。Python內置的functools模塊提供了一個函數partial,可以為任意函數生成偏函數:
代碼如下: