Python的函數式編程--從入門到⎡放棄⎦

很早之前就據說過了函數式編程,印象中是一種很晦澀難懂的編程模式,但卻一直沒有去進行了解。html

剛好這周組內的週會輪到我主持,一時也沒想到要分享什麼。靈光一閃,就選定函數式編程這個主題吧,反正組裏的同事都沒有學過,只須要講解入門方面的知識就好,也正好能夠借這個機會逼迫本身去學習下這種新的編程方式。python

通過初步瞭解,發現支持函數式編程的語言挺多的,除了像Lisp、Scheme、Haskell、Erlang這樣專用的函數式編程語言,咱們經常使用的好多通用型編程語言(如Java、Python、Ruby、Javascript等)都支持函數式編程模式。考慮了下實際狀況,最終仍是選擇Python做爲函數式編程的入門語言,由於組內同事都熟悉Python,以此做爲切入點不會產生太大困難。linux

通過查詢資料和初步學習,對函數式編程有了些概念,通過整理,便造成了分享PPT。程序員

如下即是此次分享的內容。express

目標

一般,咱們在新學習一門技術或者編程語言的時候,一般都會先從相關概念和特性入手。對於新接觸函數式編程的人來講,可能會想知道以下幾點:編程

  • 什麼是函數式編程?
  • 函數式編程的特色?
  • 函數式編程的用途?
  • 函數式編程相比於命令式編程和麪向對象編程的優缺點?

可是我此次分享卻沒有按照這個思路,由於我感受在一開始就向聽衆灌輸太多概念性的東西,反倒會讓聽衆感到迷糊。由於通過查詢資料發現,對於什麼是函數化編程,很難能有一個協調一致的定義。並且因爲我也是新接觸,自身的理解可能會存在較大的誤差。ruby

所以,我決定分享內容儘可能從你們熟悉的命令式編程切入,經過大量實例來向聽衆展示函數式編程思惟方式的不一樣之處。在這以後,再回過頭看這幾個問題,相信聽衆應該都會有更深入的理解。bash

考慮到實際狀況,本次分享但願能達成的目標是:微信

  • 瞭解函數式編程與命令式編程的主要區別
  • 掌握Python語言函數式編程的基本函數和算子
  • 會將簡單的命令式編程語句轉換爲函數式編程

命令式編程 & 函數式編程

首先從你們熟悉的命令式編程開始,咱們先回顧下平時在寫代碼時主要的情景。編程語言

其實,無論咱們的業務代碼有多複雜,都離不開如下幾類操做:

  • 函數定義:def
  • 條件控制:if, elif, else
  • 循環控制:for, break, continue, while

固然,這只是部分操做類型,除此以外還應該有類和模塊、異常處理等等。但考慮到是入門,咱們就先只關注上面這三種最多見的操做。

對應地,函數式編程也有本身的關鍵字。在Python語言中,用於函數式編程的主要由3個基本函數和1個算子。

  • 基本函數:map()、reduce()、filter()
  • 算子(operator):lambda

使人驚訝的是,僅僅採用這幾個函數和算子就基本上能夠實現任意Python程序。

固然,能實現是一回事兒,實際編碼時是否這麼寫又是另一回事兒。估計要真只採用這幾個基本單元來寫全部代碼的話,無論是在表達上仍是在閱讀上應該都挺彆扭的。不過,嘗試採用這幾個基本單元來替代上述的函數定義、條件控制、循環控制等操做,對理解函數式編程如何經過函數和遞歸表達流程控制應該會頗有幫助。

在開始嘗試將命令式編程轉換爲函數式編程以前,咱們仍是須要先熟悉下這幾個基本單元。

Python函數式編程的基本單元

lambda

lambda這個關鍵詞在不少語言中都存在。簡單地說,它能夠實現函數建立的功能。

以下即是lambda的兩種使用方式。

func1 = lambda : <expression()>
func2 = lambda x : <expression(x)>
func3 = lambda x,y : <expression(x,y)>複製代碼

在第一條語句中,採用lambda建立了一個無參的函數func1。這和下面採用def建立函數的效果是相同的。

def func1():
    <expression()>複製代碼

在第二條和第三條語句中,分別採用lambda建立了須要傳入1個參數的函數func2,以及傳入2個參數的函數func3。這和下面採用def建立函數的效果是相同的。

def func2(x):
    <expression(x)>

def func3(x,y):
    <expression(x,y)>複製代碼

須要注意的是,調用func1的時候,雖然不須要傳入參數,可是必需要帶有括號(),不然返回的只是函數的定義,而非函數執行的結果。這個和在ruby中調用無參函數時有所不一樣,但願ruby程序員引發注意。

>>> func = lambda : 123
>>> func
<function <lambda> at 0x100f4e1b8>
>>> func()
123複製代碼

另外,雖然在上面例子中都將lambda建立的函數賦值給了一個函數名,但這並非必須的。從下面的例子中你們能夠看到,不少時候咱們都是直接調用lambda建立的函數,而並無命名一個函數,這也是咱們常據說的匿名函數的由來。

map()

map()函數的常見調用形式以下所示:

map(func, iterable)複製代碼

map()須要兩個必填參數,第一個參數是一個函數名,第二個參數是一個可迭代的對象,如列表、元組等。

map()實現的功能很簡單,就是將第二個參數(iterable)中的每個元素分別傳給第一個參數(func),依次執行函數獲得結果,並將結果組成一個新的list對象後進行返回。返回結果永遠都是一個list

簡單示例以下:

>>> double_func = lambda s : s * 2
>>> map(double_func, [1,2,3,4,5])
[2, 4, 6, 8, 10]複製代碼

除了傳入一個可迭代對象這種常見的模式外,map()還支持傳入多個可迭代對象。

map(func, iterable1, iterable2)複製代碼

在傳入多個可迭代對象的狀況下,map()會依次從全部可迭代對象中依次取一個元素,組成一個元組列表,而後將元組依次傳給func;若可迭代對象的長度不一致,則會以None進行補上。

經過如下示例應該就比較容易理解。

>>> plus = lambda x,y : (x or 0) + (y or 0)
>>> map(plus, [1,2,3], [4,5,6])
[5, 7, 9]
>>> map(plus, [1,2,3,4], [4,5,6])
[5, 7, 9, 4]
>>> map(plus, [1,2,3], [4,5,6,7])
[5, 7, 9, 7]複製代碼

在上面的例子中,之因此採用x or 0的形式,是爲了防止None + int出現異常。

須要注意的是,可迭代對象的個數應該與func的參數個數一致,不然就會出現異常,由於傳參個數與函數參數個數不一致了,這個應該比較好理解。

>>> plus = lambda x,y : x + y
>>> map(plus, [1,2,3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() takes exactly 2 arguments (1 given)複製代碼

另外,map()還存在一種特殊狀況,就是func爲None。這個時候,map()仍然是從全部可迭代對象中依次取一個元素,組成一個元組列表,而後將這個元組列表做爲結果進行返回。

>>> map(None, [1,2,3,4])
[1, 2, 3, 4]
>>> map(None, [1,2,3,4], [5,6,7,8])
[(1, 5), (2, 6), (3, 7), (4, 8)]
>>> map(None, [1,2,3,4], [5,6,7])
[(1, 5), (2, 6), (3, 7), (4, None)]
>>> map(None, [1,2,3,4], [6,7,8,9], [11,12])
[(1, 6, 11), (2, 7, 12), (3, 8, None), (4, 9, None)]複製代碼

reduce()

reduce()函數的調用形式以下所示:

reduce(func, iterable[, initializer])複製代碼

reduce()函數的功能是對可迭代對象(iterable)中的元素從左到右進行累計運算,最終獲得一個數值。第三個參數initializer是初始數值,能夠空置,空置爲None時就從可迭代對象(iterable)的第二個元素開始,並將第一個元素做爲以前的結果。

文字描述可能不大清楚,看下reduce()的源碼應該就比較清晰了。

def reduce(function, iterable, initializer=None):
    it = iter(iterable)
    if initializer is None:
        try:
            initializer = next(it)
        except StopIteration:
            raise TypeError('reduce() of empty sequence with no initial value')
    accum_value = initializer
    for x in it:
        accum_value = function(accum_value, x)
    return accum_value複製代碼

再加上以下示例,對reduce()的功能應該就能掌握了。

>>> plus = lambda x, y : x + y
>>> reduce(plus, [1,2,3,4,5])
15
>>> reduce(plus, [1,2,3,4,5], 10)
25複製代碼

filter()

filter()函數的調用形式以下:

filter(func, iterable)複製代碼

filter()有且僅有兩個參數,第一個參數是一個函數名,第二個參數是一個可迭代的對象,如列表、元組等。

filter()函數的調用形式與map()比較相近,都是將第二個參數(iterable)中的每個元素分別傳給第一個參數(func),依次執行函數獲得結果;差別在於,filter()會判斷每次執行結果的bool值,並只將bool值爲true的篩選出來,組成一個新的列表並進行返回。

>>> mode2 = lambda x : x % 2
>>> filter(mode2, [1,2,3,4,5,6,7,8,9,10])
[1, 3, 5, 7, 9]複製代碼

以上即是Python函數式編程基本單元的核心內容。

接下來,咱們就開始嘗試採用新學習到的基本單元對命令式編程中的條件控制循環控制進行轉換。

替換條件控制語句

在對條件控制進行替換以前,咱們先來回顧下Python中對布爾表達式求值時進行的「短路」處理。

什麼叫「短路」處理?簡單地講,就是以下兩點:

  • f(x) and g(y)中,當f(x)false時,不會再執行g(y),直接返回false
  • f(x) or g(y)中,當f(x)true時,不會再執行g(y),直接返回true

結論是顯然易現的,就再也不過多解釋。

那麼,對應到條件控制語句,咱們不難理解,以下條件控制語句和表達式是等價的。

# flow control statement
if <cond1>:   func1()
elif <cond2>: func2()
else:         func3()複製代碼
# Equivalent "short circuit" expression
(<cond1> and func1()) or (<cond2> and func2()) or (func3())複製代碼

經過這個等價替換,咱們就去除掉了if/elif/else關鍵詞,將條件控制語句轉換爲一個表達式。那這個表達式和函數式編程有什麼關係呢?

這時咱們回顧上面講過的lambda,會發現lambda算子返回的就是一個表達式。

基於這一點,咱們就能夠採用lambda建立以下函數。

>>> pr = lambda s:s
>>> print_num = lambda x: (x==1 and pr("one")) \
....                  or (x==2 and pr("two")) \
....                  or (pr("other"))
>>> print_num(1)
'one'
>>> print_num(2)
'two'
>>> print_num(3)
'other'複製代碼

經過函數調用的結果能夠看到,以上函數實現的功能與以前的條件控制語句實現的功能徹底相同。

到這裏,咱們就實現了命令式條件控制語句向函數式語句的轉換。而且這個轉換的方法是通用的,全部條件控制語句均可以採用這種方式轉換爲函數式語句。

替換循環控制語句

接下來咱們再看循環控制語句的轉換。在Python中,循環控制是經過forwhile這兩種方式實現的。

替換for循環

for循環語句的替換十分簡單,採用map()函數就能輕鬆實現。這主要是由於for語句和map()原理相同,都是對可迭代對象裏面的每個元素進行操做,所以轉換過程比較天然。

# statement-based for loop
for e in lst:  func(e)

# Equivalent map()-based loop
map(func, lst)複製代碼
>>> square = lambda x : x * x
>>> for x in [1,2,3,4,5]: square(x)
...
1
4
9
16
25
>>> map(square, [1,2,3,4,5])
[1, 4, 9, 16, 25]複製代碼

替換while循環

while循環語句的替換相比而言就複雜了許多。

下面分別是while循環語句及其對應的函數式風格的代碼。

# statement-based while loop
while <condition>:
    <pre-suite>
    if <break_condition>:
        break
    else:
        <suite>

# Equivalent FP-style recursive while loop
def while_block():
    <pre-suite>
    if <break_condition>:
        return 1
    else:
        <suite>
    return 0

while_FP = lambda: <condition> and (while_block() or while_FP())
while_FP()複製代碼

這裏的難點在於,函數式while_FP循環採用了遞歸的概念。當<condition>true時,進入循環體,執行while_block();若<break_condition>true時,返回1,while_FP()調用結束;若<break_condition>false時,返回0,會繼續執行or右側的while_FP(),從而實現遞歸調用;若<break_condition>始終爲false,則會持續遞歸調用while_FP(),這就實現了while語句中一樣的功能。

爲了對函數式的while循環有更深入的理解,能夠再看下以下示例。這個例子是在網上找的,實現的是echo功能:輸入任意非"quit"字符時,打印輸入的字符;輸入"quit"字符時,退出程序。

➜  PythonFP python pyecho.py
IMP -- 1
1
IMP -- 2
2
IMP -- abc
abc
IMP -- 1 + 1
1 + 1
IMP -- quit
quit
➜  PythonFP複製代碼

以下即是分別採用過程式和函數式語句實現的"echo"功能。

# imperative version of "echo()"
def echo_IMP():
    while 1:
        x = raw_input("IMP -- ")
        print x
        if x == 'quit':
            break

echo_IMP()複製代碼
def monadic_print(x):
    print x
    return x

# FP version of "echo()"
echo_FP = lambda: monadic_print(raw_input("FP -- "))=='quit' or echo_FP()
echo_FP()複製代碼

更多示例

到此爲止,咱們對函數式編程總算有了點認識,到達以前設定的目標應該是沒有問題了,看來函數式編程也並無想象中的那麼難懂。

然而,這都只是函數式編程的皮毛而已,不信?再看下以下示例。

這個示例也是在網上找的,實現的是兩個列表笛卡爾積的篩選功能,找出笛卡爾積元組集合中兩個元素之積大於25的全部元組。

bigmuls = lambda xs,ys: filter(lambda (x,y):x*y > 25, combine(xs,ys))
combine = lambda xs,ys: map(None, xs*len(ys), dupelms(ys,len(xs)))
dupelms = lambda lst,n: reduce(lambda s,t:s+t, map(lambda l,n=n: [l]*n, lst))

print bigmuls([1,2,3,4],[10,15,3,22])

[(3, 10), (4, 10), (2, 15), (3, 15), (4, 15), (2, 22), (3, 22), (4, 22)]複製代碼

雖然這個例子中lambda/map/reduce/filter都是咱們已經比較熟悉了的基本單元,可是通過組合後,理解起來仍是會比較吃力。

總結

看到這裏,有的同窗就開玩笑說我這標題名稱很是貼切,《Python的函數式編程--從入門到「放棄」》,由於之後在工做中應該也不會再嘗試使用函數式編程了,^_^。

不過,我仍是以爲函數式編程挺有意思的,更高級的特性後面值得再繼續學習。即便代碼不用寫成pure函數式風格,但在某些時候局部使用lambda/map/reduce/filter也能大大簡化代碼,也是一個不錯的選擇。

另外,經過這次分享,再次切身體會到了教授是最好的學習方式,只有當你真正能將一個概念講解清楚的時候,你纔算是掌握了這個概念。

參考連接

www.ibm.com/developerwo…


關於做者

筆名九毫,英文名Leo Lee。

專一於軟件測試領域和測試開發技術,享受在牆角安靜地debug,也喜歡在博客上分享文字。

我的博客:debugtalk.com

我的微信公衆號:DebugTalk

DebugTalk

相關文章
相關標籤/搜索