01.編程學習--Python高階函數


文:鄭元春

人生苦短,我用Python!

0x00:函數式編程

describe what to do, rather than how to do it.

函數式編程(英語:functional programming)或稱函數程序設計,又稱泛函編程,是一種編程典範,它將電腦運算視為數學上的函數計算,並且避免使用程序狀態以及易變物件。函數程式語言最重要的基礎是λ演算(lambda calculus)。而且λ演算的函數可以接受函數當作輸入(引數)和輸出(傳出值)。
比起指令式編程,函數式編程更加強調程序執行的結果而非執行的過程,倡導利用若干簡單的執行單元讓計算結果不斷漸進,逐層推導復雜的運算,而不是設計一個復雜的執行過程。

函數式編程就是一種抽象程度很高的編程范式,純粹的函數式編程語言編寫的函數沒有變量,因此,任意一個函數,只要輸入是確定的,輸出就是確定的,這種純函數我們稱之為沒有副作用(邊界效應)。而允許使用變量的程序設計語言,由于函數內部的變量狀態不確定,同樣的輸入,可能得到不同的輸出,因此,這種函數是有副作用的。

三大特點
1.immutable data(不可變數據):
默認上變量是不可變的,如果你要改變變量,你需要把變量copy出去修改

2.first class functions
這個技術可以讓你的函數就像變量一樣來使用。也就是說,你的函數可以像變量一樣被創建,修改,并當成變量一樣傳遞,返回或是在函數中嵌套函數。這個有點像Javascript的Prototype

3.尾遞歸優化
我們知道遞歸的害處,那就是如果遞歸很深的話,stack受不了,并會導致性能大幅度下降。所以,我們使用尾遞歸優化技術——每次遞歸時都會重用stack,這樣一來能夠提升性能,當然,這需要語言或編譯器的支持。Python就不支持。

函數式編程的幾個技術
1.map & reduce
基于命令式的編程語言中,如果對一個數據集進行特定操作的時候,需要使用for或者是while循環,讓用戶在循環語句內部進行操作,并且所有的邊界條件都是用戶自己定義的,所以就會出現邊界溢出的bug。而map和reduce思想就是用一種更加數學化(理論化)的編程。

2.Pipeline
之前map、reduce是將一組數據放到特定的函數里面進行操作,這里的操作函數一般就一個。管道(Pipeline)就像是Linux系統中的管道一樣。數據依此通過不同的管道,最后輸出。

3.recuring
遞歸最大的好處就簡化代碼,他可以把一個復雜的問題用很簡單的代碼描述出來。注意:遞歸的精髓是描述問題,而這正是函數式編程的精髓。

4.currying
把一個函數的多個參數分解成多個函數, 然后把函數多層封裝起來,每層函數都返回一個函數去接收下一個參數這樣,可以簡化函數的多個參數。而且每一層只專注最少的功能性代碼,所以你的代碼可維護性將大大提高,出現bug的概率也會大大降低。

5.higher order function
高階函數:所謂高階函數就是函數當參數,把傳入的函數做一個封裝,然后返回這個封裝函數。現象上就是函數傳進傳出,就像面向對象對象滿天飛一樣。

6.lazy evaluation
惰性求值:這個需要編譯器的支持。表達式不在它被綁定到變量之后就立即求值,而是在該值被取用的時候求值,也就是說,語句如x:=expression; (把一個表達式的結果賦值給一個變量)明顯的調用這個表達式被計算并把結果放置到 x 中,但是先不管實際在 x 中的是什么,直到通過后面的表達式中到 x 的引用而有了對它的值的需求的時候,而后面表達式自身的求值也可以被延遲,最終為了生成讓外界看到的某個符號而計算這個快速增長的依賴樹。

7.__determinism __
確定性:所謂確定性的意思就是像數學那樣 f(x) = y ,這個函數無論在什么場景下,都會得到同樣的結果,這個我們稱之為函數的確定性。而不是像程序中的很多函數那樣,同一個參數,卻會在不同的場景下計算出不同的結果。所謂不同的場景的意思就是我們的函數會根據一些運行中的狀態信息的不同而發生變化。


總結:函數式編程更側重的是讓你著手于解決問題,而不是解決問題的過程或是方法,一般從命令式語言(大多數的是C++)入門的程序員都會養成thinking like a machine 的思維方式,命令式語言已經烙印在腦海中,就像那個冷笑話:
妻子對程序員老公說:去買點早飯,如果有西瓜,買一個。結果程序員連早餐也沒有買。
老本行是數學的程序員他們對函數式編程的理解會比較深入一些,所以大家程序員們找媳婦的話還是造個數學的姑娘吧!


0x01:函數即數據

everything is data in computer, even all functions and your mind.

對于計算機來說,尤其是CPU,送到其中的任何東西都是數據(0和1嘛)。函數或者是類的概念只是更高層次的語言模型,對于底層的硬件來說這些高層的語法和語義最終是要翻譯成指令進入流水線的。所以說函數也是數據的一種,這里的數據并不是指狹義的數據,或者是在內存塊中的地址,而是廣義上的信息流。

在Python中我們知道,我們可以將一個函數賦值給一個變量。變量可以當做參數傳遞到函數中,同時,變量也可以作為函數的返回值返回。那么這樣一來,函數也能夠當做參數傳遞到函數,或是作為返回值(裝飾器一章節會講到這個函數作為返回值)返回。

例子:

#函數作為參數
def callFunc(func):
    print ("callFunc.")
    func()
def Func():
    print ("call function")

callFunc(Fun)
#函數作為返回值
def  Func():
    def subFunc():
        print ("this is the inner function")
    return subFunc

Func()

函數作為返回值的好處之一就是可以實現延遲計算,函數返回的只是一個功能函數并沒有將運行結果返回,所以當年調用的時候,就像是被人授漁而非授魚一樣(好別扭的典故引用),只有當你需要的時候才去做計算。


0x02:高階函數

thinking like a Pythonor!

  • lambda 函數

寫過JavaScript的童鞋肯定知道匿名函數的作用了,匿名函數可以直接使用,定義的地方就是作用的地方,但是你沒法在其他的地方調用它。因為,他沒有名字,你什么也不告訴編譯器,編譯器也啥也不知道當然不會調用了。匿名函數就是為了便利才設計的。

在Python中,你可以使用lambda函數來充當匿名函數的角色。lambda語句構建的其實是一個函數對象,因為你可以將函數賦值給一個變量,所以說你可以使用變量來代替匿名函數來進行調用,當然也可以結合其他的高級函數使用。

#將匿名函數賦值給變量
p=lambda x : x+2
print (p(2))

p2=lambda x,y: x+y
print(p2(1,2))

這樣一看,lambda確實沒有什么大的作用嘛,就是省略了函數名嘛。事實上就是這樣的,下面是些匿名函數的好處:

  1. 使用Python寫一些執行腳本時,使用lambda可以省去定義函數的過程,讓代碼更加精簡。
  1. 對于一些抽象的,不會別的地方再復用的函數,有時候給函數起個名字也是個難題,使用lambda不需要考慮命名的問題。
  2. 使用lambda在某些時候讓代碼更容易理解。
  • map函數

map(function, iterable, ...)

Apply function to every item of iterable and return a list of the results. If additional iterable arguments are passed, function must take that many arguments and is applied to the items from all iterables in parallel. If one iterable is shorter than another it is assumed to be extended withNoneitems. If function isNone, the identity function is assumed; if there are multiple arguments, map() returns a list consisting of tuples containing the corresponding items from all iterables (a kind of transpose operation). The iterable arguments may be a sequence or any iterable object; the result is always a list.

先看最先一句:首先,你要有一個function(這個function就是改變iterable
data中的每個element的功能函數,比如說元素加倍,元素取反)。然后map函數會返回一個list給你。你看不到實際的循環過程,一切都是編譯器“偷偷”的完成的這些對數據的循環操作。

def doubleMe(para):
    return para*2

result=map(doubleMe,range(1,11))
#結果是result=[2,4,6,8,10,12,14,16,18,20]

#可以結合上面將的lambda函數直接寫成一行
result2=map(lambda x: x*2,range(1,11))
#結果是result2=[2,4,6,8,10,12,14,16,18,20],和上面的一致

你可以這么認為,function是個操作機器手臂,iterable data是個運行中的傳送帶,data里面的每個element就是傳送帶上面的每一個材料。第一句描述的是就是一個機器手臂,然后操作一條傳送帶(此時function只有一個參數),將每個element操作之后放在一個list中,完事之后推出這個list。

再看后面加了條件的一句:如果額外的iterable 參數需要傳遞的話,function函數需要同時去提取同位置的iterable data的元素。這一句描述的是還是一個機器手臂,但是現在有同時多個傳送帶在運行(function有多個參數),機器手臂會同時從這些傳送帶上面取數據,然后進行操作,最后還是推出list。

result=map(lambda x,y :x+y, range(1,5),range(6,10))
#結果是result=[7,9,11,13]

文檔重點描述的是,你需要同時從這些data中取相同下標的element。后面的一句,說的是假設你其中的一個data比另一個data包含的數據少,那么你會自動將短的list填充Noneitems.

result=map(lambda x,y :x+y, range(1,6),range(6,10))
#結果是會報錯誤:TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

再看最后一種情況:假如我們傳遞給map的是一個None的function會怎么樣呢,此時什么也不會發生,實際上就是做了個數據轉移操作,將數據從iterable data轉移到list中。

map(None,range(1,3))
#輸出 [1,2]
map(None,range(1,3),range(5,7))
#輸出[(1, 5), (2, 6)]
#可以看到第二個有多個參數組的情況下,只是把同下標的給“打包”了

總結:這些高級函數在高手手中真的是能省下不少力氣啊,好處是消除了for 循環的“邊界效應“,同時,和lambda函數同時使用,確實能讓人很好的閱讀和理解。像我這種從命令式語言入門的程序員來說,確實剛接觸的時候很吃驚也很費解。其實拋開你對命令式語言的執念,這些更貼近數學和”白話“的語句真的很好理解了。如果你不懂什么是命令式編程和函數式編程,這一篇《00.編程學習--初始》是從語言模型上講述兩者的區別,可以參看下。


  • reduce函數

reduce(function, iterable[, initializer])
Apply function of two arguments cumulatively to the items of iterable, from left to right, so as to reduce the iterable to a single value. If the optional initializer is present, it is placed before the items of the iterable in the calculation, and serves as a default when the iterable is empty. If initializer is not given and iterable contains only one item, the first item is returned.

map函數講述的是對傳送帶上的數據做操作之后會放到一個list里面,也就是結果的個數和傳送帶(不管有幾個傳送帶在工作)上面的數據個數是一致的。同時傳送帶上面的零件都是一樣的,比如都是汽車車門,我們的操作可以是噴漆或是焊接。有的時候我們有個機器手臂是做組裝的,而且傳送帶上面的零件都是不同類型的,比如是車門、車架、車輪、發動機之類的,最后需要組裝在一起。這里就可以用reduce函數了,他的返回是只是一個(并不是list)。
第一種:將iterable data里面的數據,從左到右,挨個取出來放到function里
,function必須接受兩個參數,一個就是取出來的這個元素,一個就是前面每次操作的結果。reduce的本意是消除,這里的意思就是將一大堆的數據挨個消除,最后整合(流水線裝配)在一起。

result = reduce(lambda x,y : x+y, range(1,5))
#結果是result=10

第二種:有的時候我們可以傳遞第三個參數,這是一個初始值,就像是有的時候前面的裝配線已經裝了部分汽車了,現在的裝配線(reduce)只需要裝內飾就行。如果沒有初始值的話,就使用iterable data的第一個element作為初始值。

result = reduce(lambda x,y : x+y, range(1,5),3)
#結果是result=13
  • filter函數

filter(function, iterable)
Construct a list from those elements of iterable for which function returns true. iterable may be either a sequence, a container which supports iteration, or an iterator. If iterable is a string or a tuple, the result also has that type; otherwise it is always a list. If function is None, the identity function is assumed, that is, all elements of iterable that are false are removed.

還是用生產線的例子講解,有的時候我們需要嚴格把控質量,需要將不合格的零件挑選出來,這時候就需要filter了(filter本意就是篩選嘛)。filter函數的作用就是將iterable data里面符合你function的element選出來。你的function必須返回True/False。如果是你的function是None的話,那么編譯器默認的function就會將iterable data 中值是False的element去除。

result = filter(lambda x: x>3, range(1,5))
#結果是result=[4]

result = filter(None,range(1,3))
#結果是result=[1,2]
result=filter(None,[False,True])
# 結果是result=[True]

總結

其實這就是Python中”流水線”操作,你可以想象成汽車裝配廠的流水線。

map 函數:噴漆流水線(可以有多條)
reduce 函數:組裝流水線(只有一條)
filter 函數:篩選流水線(只有一條)

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容