2. 從函式開始
2.1. 定義一個函式
如下定義了一個求和函式:
1 2 |
def add(x, y): return x + y |
關於引數和返回值的語法細節可以參考其他文件,這裡就略過了。
使用lambda可以定義簡單的單行匿名函式。lambda的語法是:
1 |
lambda args: expression |
引數(args)的語法與普通函式一樣,同時表示式(expression)的值就是匿名函式呼叫的返回值;而lambda表示式返回這個匿名函式。如果我們給匿名函式取個名字,就像這樣:
1 |
lambda_add = lambda x, y: x + y |
這與使用def定義的求和函式完全一樣,可以使用lambda_add作為函式名進行呼叫。然而,提供lambda的目的是為了編寫偶爾為之的、簡單的、可預見不會被修改的匿名函式。這種風格雖然看起來很酷,但並不是一個好主意,特別是當某一天需要對它進行擴充,再也無法用一個表示式寫完時。如果一開始就需要給函式命名,應該始終使用def關鍵字。
2.2. 使用函式賦值
事實上你已經見過了,上一節中我們將lambda表示式賦值給了add。同樣,使用def定義的函式也可以賦值,相當於為函式取了一個別名,並且可以使用這個別名呼叫函式:
1 2 |
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. 閉包
閉包是一類特殊的函式。如果一個函式定義在另一個函式的作用域中,並且函式中引用了外部函式的區域性變數,那麼這個函式就是一個閉包。下面的程式碼定義了一個閉包:
1 2 3 4 5 6 7 |
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可以做到這一點:
1 2 3 4 5 6 7 8 9 |
#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熱熱身:
1 |
print add('三角形的樹', '北極') |
與加法運算子不同,你一定很驚訝於答案是’三角函式’。這是一個內建的彩蛋…bazinga!
言歸正傳。我們的客戶有一個從0到4的列表:
1 |
lst = range(5) #[0, 1, 2, 3, 4] |
雖然我們在上一小節裡給了他一個加法器,但現在他仍然在為如何計算這個列表所有元素的和而苦惱。當然,對我們而言這個任務輕鬆極了:
1 2 3 |
amount = 0 for num in lst: amount = add(amount, num) |
這是一段典型的指令式風格的程式碼,一點問題都沒有,肯定可以得到正確的結果。現在,讓我們試著用函式式的風格重構一下。
首先可以預見的是求和這個動作是非常常見的,如果我們把這個動作抽象成一個單獨的函式,以後需要對另一個列表求和時,就不必再寫一遍這個套路了:
1 2 3 4 5 6 7 |
def sum_(lst): amount = 0 for num in lst: amount = add(amount, num) return amount print sum_(lst) |
還能繼續。sum_函式定義了這樣一種流程:
1. 使用初始值與列表的第一個元素相加;
2. 使用上一次相加的結果與列表的下一個元素相加;
3. 重複第二步,直到列表中沒有更多元素;
4. 將最後一次相加的結果返回。
如果現在需要求乘積,我們可以寫出類似的流程——只需要把相加換成相乘就可以了:
1 2 3 4 5 |
def multiply(lst): product = 1 for num in lst: product = product * num return product |
除了初始值換成了1以及函式add換成了乘法運算子,其他的程式碼全部都是冗餘的。我們為什麼不把這個流程抽象出來,而將加法、乘法或者其他的函式作為引數傳入呢?
1 2 3 4 5 6 7 |
def reduce_(function, lst, initial): result = initial for num in lst: result = function(result, num) return result print reduce_(add, lst, 0) |
現在,想要算出乘積,可以這樣做:
1 |
print reduce_(lambda x, y: x * y, lst, 1) |
那麼,如果想要利用reduce_找出列表中的最大值,應該怎麼做呢?請自行思考:)
雖然有模板方法這樣的設計模式,但那樣的複雜度往往使人們更情願到處編寫迴圈。將函式作為引數完全避開了模板方法的複雜度。
Python有一個內建函式reduce,完整實現並擴充套件了reduce_的功能。本文稍後的部分包含了有用的內建函式的介紹。請注意我們的目的是沒有迴圈,使用函式替代迴圈是函式式風格區別於指令式風格的最顯而易見的特徵。
*像Python這樣構建於類C語言之上的函式式語言,由於語言本身提供了編寫迴圈程式碼的能力,內建函式雖然提供函數語言程式設計的介面,但一般在內部還是使用迴圈實現的。同樣的,如果發現內建函式無法滿足你的迴圈需求,不妨也封裝它,並提供一個介面。
2.5. 作為返回值
將函式返回通常需要與閉包一起使用(即返回一個閉包)才能發揮威力。我們先看一個函式的定義:
1 2 3 4 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:
1 2 |
lst = map_(lambda x: add(1, x), lst) print reduce_(lambda x, y: x * y, lst, 1) |
答案是120,這還遠遠不夠大。再來:
1 2 |
lst = map_(lambda x: add(10, x), lst) print reduce_(lambda x, y: x * y, lst, 1) |
囧,事實上我真的沒有想到答案會是360360,我發誓沒有收周鴻禕任何好處。
現在回頭看看我們寫的兩個lambda表示式:相似度超過90%,絕對可以使用抄襲來形容。而問題不在於抄襲,在於多寫了很多字元有木有?如果有一個函式,根據你指定的左運算元,能生成一個加法函式,用起來就像這樣:
1 |
lst = map_(add_to(10), lst) #add_to(10)返回一個函式,這個函式接受一個引數並加上10後返回 |
寫起來應該會舒服不少。下面是函式add_to的實現:
1 2 |
def add_to(n): return lambda x: add(n, x) |
通過為已經存在的某個函式指定數個引數,生成一個新的函式,這個函式只需要傳入剩餘未指定的引數就能實現原函式的全部功能,這被稱為偏函式。Python內建的functools模組提供了一個函式partial,可以為任意函式生成偏函式:
1 |
functools.partial(func[, *args][, **keywords]) |
你需要指定要生成偏函式的函式、並且指定數個引數或者命名引數,然後partial將返回這個偏函式;不過嚴格的說partial返回的不是函式,而是一個像函式一樣可直接呼叫的物件,當然,這不會影響它的功能。
另外一個特殊的例子是裝飾器。裝飾器用於增強甚至乾脆改變原函式的功能,我曾寫過一篇文件介紹裝飾器,地址在這裡:http://www.cnblogs.com/huxi/archive/2011/03/01/1967600.html。
*題外話,單就例子中的這個功能而言,在一些其他的函式式語言中(例如Scala)可以使用名為柯里化(Currying)的技術實現得更優雅。柯里化是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數而且返回結果的新函式的技術。如下的虛擬碼所示:
1 2 3 4 5 |
#不是真實的程式碼 def add(x)(y): #柯里化 return x + y lst = map_(add(10), lst) |
通過將add函式柯里化,使得add接受第一個引數x,並返回一個接受第二個引數y的函式,呼叫該函式與前文中的add_to完全相同(返回x + y),且不再需要定義add_to。看上去是不是更加清爽呢?遺憾的是Python並不支援柯里化。
2.6. 部分內建函式介紹
- reduce(function, iterable[, initializer])
這個函式的主要功能與我們定義的reduce_相同。需要補充兩點:
它的第二個引數可以是任何可迭代的物件(實現了__iter__()方法的物件);
如果不指定第三個引數,則第一次呼叫function將使用iterable的前兩個元素作為引數。
由reduce和一些常見的function組合成了下面列出來的內建函式:
1 2 3 4 5 |
all(iterable) == reduce(lambda x, y: bool(x and y), iterable) any(iterable) == reduce(lambda x, y: bool(x or y), iterable) max(iterable[, args...][, key]) == reduce(lambda x, y: x if key(x) > key(y) else y, iterable_and_args) min(iterable[, args...][, key]) == reduce(lambda x, y: x if key(x) < key(y) else y, iterable_and_args) sum(iterable[, start]) == reduce(lambda x, y: x + y, iterable, start) |
- map(function, iterable, …)
這個函式的主要功能與我們定義的map_相同。需要補充一點:
map還可以接受多個iterable作為引數,在第n次呼叫function時,將使用iterable1[n], iterable2[n], …作為引數。
- filter(function, iterable)
這個函式的功能是過濾出iterable中所有以元素自身作為引數呼叫function時返回True或bool(返回值)為True的元素並以列表返回,與系列第一篇中的my_filter函式相同。
- zip(iterable1, iterable2, …)
這個函式返回一個列表,每個元素都是一個元組,包含(iterable1[n], iterable2[n], …)。
例如:zip([1, 2], [3, 4]) –> [(1, 3), (2, 4)]
如果引數的長度不一致,將在最短的序列結束時結束;如果不提供引數,將返回空列表。
除此之外,你還可以使用本文2.5節中提到的functools.partial()為這些內建函式建立常用的偏函式。
另外,pypi上有一個名為functional的模組,除了這些內建函式外,還額外提供了更多的有意思的函式。但由於使用的場合並不多,並且需要額外安裝,在本文中就不介紹了。但我仍然推薦大家下載這個模組的純Python實現的原始碼看看,開闊思維嘛。裡面的函式都非常短,原始檔總共只有300行不到,地址在這裡:http://pypi.python.org/pypi/functional
此篇結束:)