Python函數式編程

當咱們提及函數式編程來講,咱們會看到以下函數式編程的長相:javascript

  • 函數式編程的三大特性:
    • immutable data 不可變數據:像Clojure同樣,默認上變量是不可變的,若是你要改變變量,你須要把變量copy出去修改。這樣一來,可讓你的程序少不少Bug。由於,程序中的狀態很差維護,在併發的時候更很差維護。(你能夠試想一下若是你的程序有個複雜的狀態,當之後別人改你代碼的時候,是很容易出bug的,在並行中這樣的問題就更多了)
    • first class functions:這個技術可讓你的函數就像變量同樣來使用。也就是說,你的函數能夠像變量同樣被建立,修改,並當成變量同樣傳遞,返回或是在函數中嵌套函數。這個有點像Javascript的Prototype(參看Javascript的面向對象編程
    • 尾遞歸優化:咱們知道遞歸的害處,那就是若是遞歸很深的話,stack受不了,並會致使性能大幅度降低。因此,咱們使用尾遞歸優化技術——每次遞歸時都會重用stack,這樣一來可以提高性能,固然,這須要語言或編譯器的支持。Python就不支持。
  • 函數式編程的幾個技術
    • map & reduce :這個技術不用多說了,函數式編程最多見的技術就是對一個集合作Map和Reduce操做。這比起過程式的語言來講,在代碼上要更容易閱讀。(傳統過程式的語言須要使用for/while循環,而後在各類變量中把數據倒過來倒過去的)這個很像C++中的STL中的foreach,find_if,count_if之流的函數的玩法。
    • pipeline:這個技術的意思是,把函數實例成一個一個的action,而後,把一組action放到一個數組或是列表中,而後把數據傳給這個action list,數據就像一個pipeline同樣順序地被各個函數所操做,最終獲得咱們想要的結果。
    • recursing 遞歸 :遞歸最大的好處就簡化代碼,他能夠把一個複雜的問題用很簡單的代碼描述出來。注意:遞歸的精髓是描述問題,而這正是函數式編程的精髓。
    • currying:把一個函數的多個參數分解成多個函數, 而後把函數多層封裝起來,每層函數都返回一個函數去接收下一個參數這樣,能夠簡化函數的多個參數。在C++中,這個很像STL中的bind_1st或是bind2nd。
    • higher order function 高階函數:所謂高階函數就是函數當參數,把傳入的函數作一個封裝,而後返回這個封裝函數。現象上就是函數傳進傳出,就像面向對象對象滿天飛同樣。

 

  • 還有函數式的一些好處
    • parallelization 並行:所謂並行的意思就是在並行環境下,各個線程之間不須要同步或互斥。
    • lazy evaluation 惰性求值:這個須要編譯器的支持。表達式不在它被綁定到變量以後就當即求值,而是在該值被取用的時候求值,也就是說,語句如x:=expression; (把一個表達式的結果賦值給一個變量)明顯的調用這個表達式被計算並把結果放置到 x 中,可是先無論實際在 x 中的是什麼,直到經過後面的表達式中到 x 的引用而有了對它的值的需求的時候,然後面表達式自身的求值也能夠被延遲,最終爲了生成讓外界看到的某個符號而計算這個快速增加的依賴樹。
    • determinism 肯定性:所謂肯定性的意思就是像數學那樣 f(x) = y ,這個函數不管在什麼場景下,都會獲得一樣的結果,這個咱們稱之爲函數的肯定性。而不是像程序中的不少函數那樣,同一個參數,卻會在不一樣的場景下計算出不一樣的結果。所謂不一樣的場景的意思就是咱們的函數會根據一些運行中的狀態信息的不一樣而發生變化。

上面的那些東西太抽象了,仍是讓咱們來循序漸近地看一些例子吧。html

咱們先用一個最簡單的例子來講明一下什麼是函數式編程。java

先看一個非函數式的例子:python

1
2
3
4
int cnt;
void increment(){
     cnt++;
}

那麼,函數式的應該怎麼寫呢?ios

1
2
3
int increment( int cnt){
     return cnt+1;
}

你可能會以爲這個例子太普通了。是的,這個例子就是函數式編程的準則:不依賴於外部的數據,並且也不改變外部數據的值,而是返回一個新的值給你shell

咱們再來看一個簡單例子:express

1
2
3
4
5
6
7
8
9
10
def inc(x):
     def incx(y):
         return x + y
     return incx
 
inc2 = inc( 2 )
inc5 = inc( 5 )
 
print inc2( 5 ) # 輸出 7
print inc5( 5 ) # 輸出 10

咱們能夠看到上面那個例子inc()函數返回了另外一個函數incx(),因而咱們能夠用inc()函數來構造各類版本的inc函數,好比:inc2()和inc5()。這個技術其實就是上面所說的Currying技術。從這個技術上,你可能體會到函數式編程的理念:把函數當成變量來用,關注於描述問題而不是怎麼實現,這樣可讓代碼更易讀。編程

Map & Reduce

在函數式編程中,咱們不該該用循環迭代的方式,咱們應該用更爲高級的方法,以下所示的Python代碼數組

1
2
3
name_len = map ( len , [ "hao" , "chen" , "coolshell" ])
print name_len
# 輸出 [3, 4, 9]

你能夠看到這樣的代碼很易讀,由於,這樣的代碼是在描述要幹什麼,而不是怎麼幹bash

咱們再來看一個Python代碼的例子:

1
2
3
4
5
6
def toUpper(item):
       return item.upper()
 
upper_name = map (toUpper, [ "hao" , "chen" , "coolshell" ])
print upper_name
# 輸出 ['HAO', 'CHEN', 'COOLSHELL']

順便說一下,上面的例子個是否是和咱們的STL的transform有些像?

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
 
int main() {
   string s= "hello" ;
   string out;
   transform(s.begin(), s.end(), back_inserter(out), :: toupper );
   cout << out << endl;
   // 輸出:HELLO
}

在上面Python的那個例子中咱們能夠看到,咱們寫義了一個函數toUpper,這個函數沒有改變傳進來的值,只是把傳進來的值作個簡單的操做,而後返回。而後,咱們把其用在map函數中,就能夠很清楚地描述出咱們想要幹什麼。而不會去理解一個在循環中的怎麼實現的代碼,最終在讀了不少循環的邏輯後才發現原來是這個或那個意思。 下面,咱們看看描述實現方法的過程式編程是怎麼玩的(看上去是否是不如函數式的清晰?):

1
2
3
4
upname = [ 'HAO' , 'CHEN' , 'COOLSHELL' ]
lowname = []
for i in range ( len (upname)):
     lowname.append( upname[i].lower() )

對於map咱們別忘了lambda表達式:你能夠簡單地理解爲這是一個inline的匿名函數。下面的lambda表達式至關於:def func(x): return x*x

1
2
3
squares = map ( lambda x: x * x, range ( 9 ))
print squares
# 輸出 [0, 1, 4, 9, 16, 25, 36, 49, 64]

咱們再來看看reduce怎麼玩?(下面的lambda表達式中有兩個參數,也就是說每次從列表中取兩個值,計算結果後把這個值再放回去,下面的表達式至關於:((((1+2)+3)+4)+5) )

1
2
print reduce ( lambda x, y: x + y, [ 1 , 2 , 3 , 4 , 5 ])
# 輸出 15

Python中的除了map和reduce外,還有一些別的如filter, find, all, any的函數作輔助(其它函數式的語言也有),可讓你的代碼更簡潔,更易讀。 咱們再來看一個比較複雜的例子:

計算數組中正數的平均值
1
2
3
4
5
6
7
8
9
10
11
12
13
num = [ 2 , - 5 , 9 , 7 , - 2 , 5 , 3 , 1 , 0 , - 3 , 8 ]
positive_num_cnt = 0
positive_num_sum = 0
for i in range ( len (num)):
     if num[i] > 0 :
         positive_num_cnt + = 1
         positive_num_sum + = num[i]
 
if positive_num_cnt > 0 :
     average = positive_num_sum / positive_num_cnt
 
print average
# 輸出 5

若是用函數式編程,這個例子能夠寫成這樣:

1
2
positive_num = filter ( lambda x: x> 0 , num)
average = reduce ( lambda x,y: x + y, positive_num) / len ( positive_num )

C++11玩的法:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <algorithm>
#include <numeric>
#include <string>
#include <vector>
using namespace std;
 
vector num {2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8};
vector p_num;
copy_if(num.begin(), num.end(), back_inserter(p_num), []( int i){ return (i>0);} );
int average = accumulate(p_num.begin(), p_num.end(), 0) / p_num.size();
cout << "averge: " << average << endl;

咱們能夠看到,函數式編程有以下好處:

1)代碼更簡單了。
2)數據集,操做,返回值都放到了一塊兒。
3)你在讀代碼的時候,沒有了循環體,因而就能夠少了些臨時變量,以及變量倒來倒去邏輯。
4)你的代碼變成了在描述你要幹什麼,而不是怎麼去幹。

最後,咱們來看一下Map/Reduce這樣的函數是怎麼來實現的(下面是Javascript代碼)

map函數
1
2
3
4
5
6
7
var map = function (mappingFunction, list) {
   var result = [];
   forEach(list, function (item) {
     result.push(mappingFunction(item));
   });
   return result;
};

下面是reduce函數的javascript實現(謝謝 @下雨在家 修正的我原來的簡單版本)

reduce函數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function reduce(actionFunction, list, initial){
     var accumulate;
     var temp;
     if (initial){
         accumulate = initial;
     }
     else {
         accumulate = list.shfit();
     }
     temp = list.shift();
     while (temp){
         accumulate = actionFunction(accumulate,temp);
         temp = list.shift();
     }
     return accumulate;
};

Declarative Programming vs Imperative Programming

前面提到過屢次的函數式編程關注的是:describe what to do, rather than how to do it. 因而,咱們把之前的過程式的編程範式叫作 Imperative Programming – 指令式編程,而把函數式的這種範式叫作 Declarative Programming – 聲明式編程。

下面咱們看一下相關的示例(本示例來自這篇文章 )。

好比,咱們有3輛車比賽,簡單起見,咱們分別給這3輛車有70%的機率能夠往前走一步,一共有5次機會,咱們打出每一次這3輛車的前行狀態。

對於Imperative Programming來講,代碼以下(Python):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from random import random
 
time = 5
car_positions = [ 1 , 1 , 1 ]
 
while time:
     # decrease time
     time - = 1
 
     print ''
     for i in range ( len (car_positions)):
         # move car
         if random() > 0.3 :
             car_positions[i] + = 1
 
         # draw car
         print '-' * car_positions[i]

咱們能夠把這個兩重循環變成一些函數模塊,這樣有利於咱們更容易地閱讀代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from random import random
 
def move_cars():
     for i, _ in enumerate (car_positions):
         if random() > 0.3 :
             car_positions[i] + = 1
 
def draw_car(car_position):
     print '-' * car_position
 
def run_step_of_race():
     global time
     time - = 1
     move_cars()
 
def draw():
     print ''
     for car_position in car_positions:
         draw_car(car_position)
 
time = 5
car_positions = [ 1 , 1 , 1 ]
 
while time:
     run_step_of_race()
     draw()

上面的代碼,咱們能夠從主循環開始,咱們能夠很清楚地看到程序的主幹,由於咱們把程序的邏輯分紅了幾個函數,這樣一來,咱們的代碼邏輯也會變得幾個小碎片,因而咱們讀代碼時要考慮的上下文就少了不少,閱讀代碼也會更容易。不像第一個示例,若是沒有註釋和說明,你仍是須要花些時間理解一下。而把代碼邏輯封裝成了函數後,咱們就至關於給每一個相對獨立的程序邏輯取了個名字,因而代碼成了自解釋的

可是,你會發現,封裝成函數後,這些函數都會依賴於共享的變量來同步其狀態。因而,咱們在讀代碼的過程時,每當咱們進入到函數裏,一量讀到訪問了一個外部的變量,咱們立刻要去查看這個變量的上下文,而後還要在大腦裏推演這個變量的狀態, 咱們才知道程序的真正邏輯。也就是說,這些函數間必需知道其它函數是怎麼修改它們之間的共享變量的,因此,這些函數是有狀態的

咱們知道,有狀態並非一件很好的事情,不管是對代碼重用,仍是對代碼的並行來講,都是有反作用的。所以,咱們要想個方法把這些狀態搞掉,因而出現了咱們的 Functional Programming 的編程範式。下面,咱們來看看函數式的方式應該怎麼寫?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from random import random
 
def move_cars(car_positions):
     return map ( lambda x: x + 1 if random() > 0.3 else x,
                car_positions)
 
def output_car(car_position):
     return '-' * car_position
 
def run_step_of_race(state):
     return { 'time' : state[ 'time' ] - 1 ,
             'car_positions' : move_cars(state[ 'car_positions' ])}
 
def draw(state):
     print ''
     print '\n' .join( map (output_car, state[ 'car_positions' ]))
 
def race(state):
     draw(state)
     if state[ 'time' ]:
         race(run_step_of_race(state))
 
race({ 'time' : 5 ,
       'car_positions' : [ 1 , 1 , 1 ]})

上面的代碼依然把程序的邏輯分紅了函數,不過這些函數都是functional的。由於它們有三個症狀:

1)它們之間沒有共享的變量。
2)函數間經過參數和返回值來傳遞數據。
3)在函數裏沒有臨時變量。

咱們還能夠看到,for循環被遞歸取代了(見race函數)—— 遞歸是函數式編程中帶用到的技術,正如前面所說的,遞歸的本質就是描述問題是什麼。

Pipeline

pipeline 管道借鑑於Unix Shell的管道操做——把若干個命令串起來,前面命令的輸出成爲後面命令的輸入,如此完成一個流式計算。(注:管道絕對是一個偉大的發明,他的設哲學就是KISS – 讓每一個功能就作一件事,並把這件事作到極致,軟件或程序的拼裝會變得更爲簡單和直觀。這個設計理念影響很是深遠,包括今天的Web Service,雲計算,以及大數據的流式計算等等)

好比,咱們以下的shell命令:

1
ps auwwx | awk '{print $2}' | sort -n | xargs echo

若是咱們抽象成函數式的語言,就像下面這樣:

1
xargs(  echo, sort(n, awk( 'print $2' , ps(auwwx)))  )

也能夠相似下面這個樣子:

1
pids = for_each(result, [ps_auwwx, awk_p2, sort_n, xargs_echo])

好了,讓咱們來看看函數式編程的Pipeline怎麼玩?

咱們先來看一個以下的程序,這個程序的process()有三個步驟:

1)找出偶數。
2)乘以3
3)轉成字符串返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def process(num):
     # filter out non-evens
     if num % 2 ! = 0 :
         return
     num = num * 3
     num = 'The Number: %s' % num
     return num
 
nums = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]
 
for num in nums:
     print process(num)
 
# 輸出:
# None
# The Number: 6
# None
# The Number: 12
# None
# The Number: 18
# None
# The Number: 24
# None
# The Number: 30

咱們能夠看到,輸出的並不夠完美,另外,代碼閱讀上若是沒有註釋,你也會比較暈。下面,咱們來看看函數式的pipeline(第一種方式)應該怎麼寫?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def even_filter(nums):
     for num in nums:
         if num % 2 = = 0 :
             yield num
def multiply_by_three(nums):
     for num in nums:
         yield num * 3
def convert_to_string(nums):
     for num in nums:
         yield 'The Number: %s' % num
 
nums = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]
pipeline = convert_to_string(multiply_by_three(even_filter(nums)))
for num in pipeline:
     print num
# 輸出:
# The Number: 6
# The Number: 12
# The Number: 18
# The Number: 24
# The Number: 30

咱們動用了Python的關鍵字 yield,這個關鍵字主要是返回一個Generator,yield 是一個相似 return 的關鍵字,只是這個函數返回的是個Generator-生成器。所謂生成器的意思是,yield返回的是一個可迭代的對象,並無真正的執行函數。也就是說,只有其返回的迭代對象被真正迭代時,yield函數纔會正真的運行,運行到yield語句時就會停住,而後等下一次的迭代。(這個是個比較詭異的關鍵字)這就是lazy evluation。

好了,根據前面的原則——「使用Map & Reduce,不要使用循環」,那咱們用比較純樸的Map & Reduce吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def even_filter(nums):
     return filter ( lambda x: x % 2 = = 0 , nums)
 
def multiply_by_three(nums):
     return map ( lambda x: x * 3 , nums)
 
def convert_to_string(nums):
     return map ( lambda x: 'The Number: %s' % x,  nums)
 
nums = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]
pipeline = convert_to_string(
                multiply_by_three(
                    even_filter(nums)
                )
             )
for num in pipeline:
     print num

可是他們的代碼須要嵌套使用函數,這個有點不爽,若是咱們能像下面這個樣子就行了(第二種方式)。

1
2
3
pipeline_func(nums, [even_filter,
                      multiply_by_three,
                      convert_to_string])

那麼,pipeline_func 實現以下:

1
2
3
4
def pipeline_func(data, fns):
     return reduce ( lambda a, x: x(a),
                   fns,
                   data)

好了,在讀過這麼多的程序後,你能夠回頭看一下這篇文章的開頭對函數式編程的描述,可能你就更有感受了。

最後,我但願這篇淺顯易懂的文章能讓你感覺到函數式編程的思想,就像OO編程,泛型編程,過程式編程同樣,咱們不用太糾結是否是咱們的程序就是OO,就是functional的,咱們重要的品味其中的味道

 

轉自:http://coolshell.cn/articles/10822.html (陳皓)

相關文章
相關標籤/搜索