當前位置:
首頁 > 科技 > 10分鐘快速入門Python函數式編程

10分鐘快速入門Python函數式編程

作者 | Brandon Skerritt

譯者 | 王天宇、琥珀

編輯 | 琥珀

出品 | AI科技大本營

本文,你會了解到什麼是函數式編程,以及如何用 Python 進行函數式編程。你還會了解到列表解析和其他形式的解析。

▌編程式函數

在命令式編程中,你需要給計算機一系列任務,然後計算機會一一執行。在執行過程中,計算機可以改變其狀態。舉個例子,假設你將 A 的初始值設為 5,接下來你還可以改變 A 的值。在變數內部值變化的層面來講,你可以掌控這些變數。

在函數式編程中,你無需告訴計算機去做什麼,而是為它提供一些必要的信息。如什麼是一個數字的最大公約數,1 到 n 的乘積是多少等等。

由於這樣,變數就無法改變了。一旦你設置了一個變數,它就會永遠保持初始狀態(注意:在純函數式語言中,它們不叫作變數)。因此在函數式編程中,函數不會產生「副作用」。「副作用」是指函數可能會修改外部變數的值。讓我們通過一個典型的 Python 例子來看一下:

a =3

defsome_func():

globala

a =5

some_func()

print(a)

這段代碼的輸出結果是5。在函數式編程中,改變變數是大忌,而且讓函數改變外部變數也是絕對禁止的。函數唯一能做的事是執行計算然後返回結果。

現在你可能在想:沒有變數,就沒有副作用嗎?為什麼這麼做很管用?好問題,下面我們簡單講一下這個問題。

如果一個函數伴隨著相同參數被調用兩次,它一定會返回一樣的結果。如果你對數學上的函數有所了解,你就會理解這裡的意義,這被稱作引用透明性。因為函數沒有副作用,如果你創建了一個可以執行計算的程序,你就可以使該程序提升性能。如果程序知道 func(2) 等於 3,我們可以把這一信息存入表中。這麼做可以防止在我們已經知道答案的情況下,程序依然反覆運行同一函數。

一般來說,在函數式編程中,我們不使用循環。而是用遞歸。遞歸是一個數學概念,我們通常將其理解為「自己喂自己」。在一個遞歸函數中,函數將自己作為子函數反覆調用。這裡有一個易於理解的遞歸函數的 Python 例子:

deffactorial_recursive(n):

# Base case: 1! = 1

ifn ==1:

return1

# Recursive case: n! = n * (n-1)!

else:

returnn * factorial_recursive(n-1)

▌Map

若要理解 map,我們要先看看 iterable 是什麼。iterable 指一類可以進行迭代的對象。通常來看,它們是列表或數組,但 Python 有許多不同類型的 iterable。你甚至可以創建自己的 iterable 對象,來執行各種魔術方法 (magic method)。魔術方法可以是一個 API,來使你的對象更加 Pythonic。你需要用兩個魔術方法來使對象成為 iterable:

classCounter:

def__init__(self, low, high):

# set class attributes inside the magic method __init__

# for "inistalise"

self.current = low

self.high = high

def__iter__(self):

# first magic method to make this object iterable

returnself

def__next__(self):

# second magic method

ifself.current > self.high:

raiseStopIteration

else:

self.current +=1

returnself.current -1

第一個魔術方法 __iter__ ,或者說 "dunder"(指以雙下劃線 「__」 作為名字開頭和結尾的方法),返回了迭代對象,這常常也被當做循環的開端。dunder 方法接下來會返回下一個對象。

讓我們快速查看一下終端會話的結果:

forcinCounter(3,8):

print(c)

輸出結果為:

3

4

5

6

7

8

在 Python 中,迭代器指只包含一個魔術方法 __iter__ 的對象。這意味著你可以訪問該對象的任何部分,但不能對其循環訪問。有些對象包含魔術方法 __next__,以及除了 __iter__ 以外的魔術方法,如 sets(下文會進行詳細討論)。在本文中,假定我們涉及的所有東西都是可迭代的對象。

那麼現在我們知道了什麼是可迭代對象,再回頭看一下 map 函數。map 函數可以讓我們在同一個 iterable 對象中,把函數作用在每一個元素上。我們通常將函數作用於列表中的每個元素,但這對大多數 iterable 對象也是可行的。Map 需要兩個輸入,分別是要執行的函數和 iterable 對象。

map(function, iterable)

假設我們有一個如下的數字列表:

[1,2,3,4,5]

然後計算每個數字的平方,我們可以寫下面一段代碼:

x = [1,2,3,4,5]

defsquare(num):

returnnum*num

print(list(map(square, x)))

Python 中的函數式函數也有「懶」的特性。如果我們不引入 "list()",那函數就會存取 iterable 對象,而不是存取列表本身。我們需要明確告訴 Python 程序 「將其轉換成列表」 ,從而供我們使用。

聽起來可能有點奇怪,我們對 Python 的評價從 「一點也不懶」 突然轉變到 「懶」。如果你對函數式編程的感悟勝過指令式編程,最終你會習慣這種轉變的。

現在我們可以很容易寫出一個像 "square(num)" 這樣的函數了,但看起來不太合適。我們有必要定義一個函數僅僅為了在 map 中調用它一次嗎?好吧,我們可以基於 lambda 在 map 中定義一個函數。


▌Lambda表達式

lambda 表達式是一個單行的函數。以一個計算數字平方的 lambda 表達式為例:

square =lambdax: x * x

現在執行這行代碼:

>>> square(3)

9

我已經聽見你在問了,參數在哪?這到底是怎麼回事?它看起來並不像個函數?

這可能有點讓人困擾,但可以解釋得清楚。首先我們給變數 "square"賦一個值,例如:

我們告訴 Python 這是一個 lambda 函數,且輸入值為 x。冒號後面的部分代表對輸入要做的事情,然後它就會返回得到的結果。

我們可以將該計算平方值的程序簡化成一行:

由此可見,在一個 lambda 表達式中,所有參數都在左邊,你想要對其執行的指令都在右邊。不得不承認這樣看起來有點雜亂。實際上這是一種很受歡迎的編程方式,只有其他的函數式程序員可以讀懂代碼。同時,把一個函數轉化成單行表達式真的很酷。


▌Reduce

Reduce 是將 iterable 轉換成一個結果的函數。通常用作來對一個列表進行計算,將其縮減為一個數字。如下:

我們可以將 lambda 表達式用作函數,事實上我們通常也是這麼做的。

一個列表的乘積為每個單獨的數字相乘在一起的結果。你可以通過如下程序實現:

但基於 reduce 你可以將上面程序寫作:

得到的乘積結果是一樣的。基於對函數式編程的理解,代碼量減小了,代碼也變得更簡潔。


▌Filter

filter 函數用於傳入一個 iterable,並過濾掉這個 iterable 中所有你不想要的序列。

通常,filter 函數傳入一個函數和一個列表。將該函數作用在列表中的任意一個元素上,如果該函數返回 True,不做任何事情。如果返回 False,將該元素從列表中刪除。

語法如下:

案例:(不使用 filter)

(使用 filter)


▌高階函數

高階函數可以將函數作為參數傳入並返回,如下:

再比如:

你之前知道我提到的純函數式編程語言沒有變數是怎麼說的嗎?高階序列函數能讓這件事變得更為簡單。你不需要儲存一個變數,如果你就是為了將數據通過函數的管道進行傳遞。

Python 中的所有函數都是一等對象。一等對象具有以下一種或多種特徵:

運行時創建

將變數或元素賦值在一個數據結構中

作為一個參數傳遞給一個函數

作為函數結果返回

因此,Python 中的所有函數都是第一類且可以作為高階函數使用。


▌偏函數應用

偏函數應用(又叫閉包)有點難理解,但超級酷。你可以調用一個函數而無需提供它所需要的全部參數。先來看看這個例子:我們想創建一個函數,傳入的兩個參數分別是 base 和 exponet,然後返回 base 的 exponent 次方。如下:

現在,我們想要一個專用的平方函數,使用 power 函數得到一個數字的平方。如下:

這個方式可以,但如果我們想要一個三次方函數呢?或者是四次方?我們能一直這樣寫嗎?當然,你是可以的。但程序員可沒那麼勤快。如果你一遍又一遍地重複做一件事,那麼你就需要用一種更高效率的方式做事,無需重複。因此,我們採用了閉包的方法。以下是一個採用閉包的平方函數的例子:

這樣是不是很酷!通過告訴 Python 第二個參數是什麼,我們只用一個參數就能調用需要兩個參數的函數。

我們還能用一個 loop,產生一個乘方函數以實現從三次方到 1000 次方的計算:


▌有時,函數式編程無法與Python相匹配

你可能已經注意到了,我們想要在函數式編程中完成的事情都會列表相關。除了 reduce 函數和偏函數應用外,所有你看到的函數都會產生列表。Python 之父 Guido 不喜歡 Python 當中的函數式編程部分,因為 Python 已經產生自己列表的方式。

如果你在 Python 的命令執行環境中輸入「import this」,你會得到如下提示:

這就是 Python 的精妙所在。在 Python 環境中,map&filter 可以實現列表解析式同樣的事情。這個打破了 Python 的一條規則,於是這部分的函數式編程看起來不那麼 Pythonic了。

另一個需要討論的是 lambda。在 Python 中,一個 lambda 函數是一個常函數。Lambda 實際上是一個語法糖,因此二者是等價的:

理論上,一個常函數可以實現一個 lambda 函數能實現的任何事情,但反過來卻不行——一個 lambda 函數無法實現一個常函數所能做的所有事情。這也是為什麼大家會有爭論函數式編程不能很好地與整個 Python 生態系統匹配。


▌列表解析式

列表解析式是 Python 產生列表的一種方式,語法如下:

然後將列表中的所有數字進行平方:

這樣,可以看到如何將一個函數作用於列表中的每一個元素。如果利用 filter 呢?請先看下此前出現過的代碼:

然後將其轉換成列表解析式:

列表解析式支持這樣的 if 表達式。你不需要通過上百萬個函數最終得到你想要的。

那麼如果想要將列表中的所有數字進行平方呢?採用 lambda、map 、 filter 你會這麼寫:

這樣看起來相當冗長且複雜。如果用列表解析式只需要這樣:


▌其他解析式

你可以創建任何一個 iterable 的解析式。

通過解析式可產生任何一個 iterable。從 Python 2.7 開始,你甚至可以創作出一本字典了。

如果這是一個 iterable,那麼就能實現。我們最後來看一個關於集合(sets)的例子。

集合是元素列表,且沒有重複出現兩次的元素。

集合的排序無關緊要。

你可能會注意到集合具有和字典中一樣的花括弧。這就在於 Python 的智能性,它會根據你是否提供了額外的值以判斷你寫的是dictionary comprehension 還是 set comprehension。


▌總結

函數式編程是優雅而簡潔的。函數式代碼可以非常簡潔,但也可以非常凌亂。一些 Python 程序員不喜歡用 Python 函數式解析。因此,你應該用你想用的,用最好的工具完成任務。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 AI科技大本營 的精彩文章:

基於知識圖譜的人機對話系統方法與實踐
特朗普「模仿」奧巴馬?進階版換臉技術DeepFakes來了

TAG:AI科技大本營 |