【轉(zhuǎn)載】深入淺出 Python 函數(shù)式編程

https://my.oschina.net/leejun2005/blog/503562

深入淺出 Python 函數(shù)式編程

1、函數(shù)式編程的定義與由來

如果程序中的函數(shù)僅接受輸入并產(chǎn)生輸出,即輸出只依賴于輸入,數(shù)據(jù)不可變,避免保存程序狀態(tài),那么就稱為函數(shù)式編程(Functional Programming,簡稱FP,又稱泛函編程)。

這種風(fēng)格也稱聲明式編程(Declarative Programming),與之相對的是指令式編程(Imperative Programming),后者中的對象會不斷修改自身狀態(tài)。函數(shù)式編程強(qiáng)調(diào)程序的執(zhí)行結(jié)果比執(zhí)行過程更重要,倡導(dǎo)利用若干簡單的執(zhí)行單元讓計算結(jié)果不斷漸進(jìn),逐層推導(dǎo)復(fù)雜的運算,而不是設(shè)計一個復(fù)雜的執(zhí)行過程。

函數(shù)編程語言最重要的基礎(chǔ)是λ演算(lambda calculus),而且λ演算的函數(shù)可以接受函數(shù)當(dāng)作輸入(引數(shù))和輸出(傳出值)。

函數(shù)式編程歷史悠久,最古老的例子莫過于1958年被創(chuàng)造出來的LISP了。而隨著程序結(jié)構(gòu)復(fù)雜,面向?qū)ο缶幊檀笮衅涞馈=陙恚啙嵍姨貏e適合計算任務(wù)的函數(shù)式編程又重新崛起,不僅僅是純粹的函數(shù)式語言如Haskell、Clojure等,各種流行語言javascripts、python、Objective-C、C#.Net甚至Java都紛紛吸收函數(shù)式編程的部分形式。而且,不僅僅是計算任務(wù),近年還出現(xiàn)了用FP編寫的UI應(yīng)用程序,如LightTable等。

本文作者@申導(dǎo) 主要采用Python語言為例,是因為它雖然不是純粹的FP,但Python能夠勝任各種編程形式,簡潔優(yōu)雅,通俗易懂。特別適合從主流語言轉(zhuǎn)過來的學(xué)習(xí)者。
2、純函數(shù)(Pure functions)

純函數(shù)指的是沒有副作用(內(nèi)存或I/O)的函數(shù),函數(shù)會返回新值,而不會修改原來的值。函數(shù)間無共享變量(不像面向?qū)ο螅?/p>

來看個非函數(shù)式的例子,它改變了變量的值。

int cnt;
void inc() {
cnt++;
}

函數(shù)式的例子,不改變變量的值,而是返回一個新值。

int cnt;
int inc() {
return cnt+1;
}

這個特點可以用來優(yōu)化代碼。例如,一個無副作用的純函數(shù),其執(zhí)行結(jié)果具有不變性,那么其執(zhí)行結(jié)果就可以緩存起來,供下次調(diào)用。再比如,兩個互不依賴的純函數(shù),其執(zhí)行順序可以互換,甚至并行地執(zhí)行而無需互斥。

在python中,不可變的元組(tuple)數(shù)據(jù)結(jié)構(gòu)特別適合函數(shù)式編程。
3、高階函數(shù)(Higher-order functions)

在FP中,首要特點就是將函數(shù)視為一等公民,函數(shù)可以當(dāng)做參數(shù)來進(jìn)行傳遞,形成所謂的高階函數(shù),形如 z=g(f(x),y),還能像變量一樣被創(chuàng)建和修改。

這種形式在非純粹的函數(shù)式編程語言里面多有吸收,用于簡化語法。連最古板的面向?qū)ο笳Z言Java也終于在Java8中引入了lambda。

讀者如果使用過C語言,一定記得標(biāo)準(zhǔn)庫中的快排函數(shù),其中第4個參數(shù)是一個函數(shù)指針,用于傳入一個比較(compare)函數(shù),而排序動作被抽象成了一個模板函數(shù)。這就是一個典型的高階函數(shù):qsort(compare(items))

void qsort(void items, size_t nitems, size_t size, int (compare)(const void , const void));

4、lambda(匿名λ函數(shù))

使用lambda可以定義簡單的單行匿名函數(shù)。lambda的語法如下:lambda args: expression

lambda_add = lambda x, y: x + y

def normal_add(x,y):
return x+y

assert lambda_add(2,3) == normal_add(2,3)

匿名λ函數(shù)與使用def定義的函數(shù)完全一樣,可以使用lambda_add作為函數(shù)名進(jìn)行調(diào)用。然而,提供lambda的目的是為了編寫偶爾為之的、簡單的、可預(yù)見不會被修改的匿名函數(shù)。
5、reduce函數(shù)

考慮一個求和的例子,一般會采用循環(huán):

def my_sum(numbers):
total = 0
for x in numbers:
total = total + x
return total

my_sum(range(1, 100))

如果再求乘積呢?

def my_product(numbers):
total = 1
for x in numbers:
total = total * x
return total

my_product(range(1, 100))

想到DRY原則,上述兩段函數(shù)存在了不少重復(fù)。我們看到除了初始值和運算符不同,其實整體的流程是差不多的,那么歸納(reduce)一下如何?

def my_reduce(numbers, function, initial):
total = initial
for x in numbers:
total = function(total, x)
return total

my_reduce(range(1, 100), lambda t,x: t+x, 0)
my_reduce(range(1, 100), lambda t,x: t*x, 1)

Python內(nèi)置的reduce(function, iterable[, initializer])函數(shù)已經(jīng)實現(xiàn)了對列表元素依次歸納的場景,而內(nèi)置的all(),any(),sum(),max(),min()等函數(shù)都是基于它衍生而來。
6、map、zip、filter函數(shù)

Python內(nèi)置函數(shù)還有map(function, iterable, ...)了,它抽象了另一種情景,即遍歷列表中的每個元素,對每個元素執(zhí)行傳入的函數(shù),并返回包含所有新元素的新列表。

而zip(iterable1, iterable2, ...)函數(shù)則對多個列表進(jìn)行合并,每個列表的第n個元素組成一個元組(tuple),然后返回包含這些元組的新列表

filter(function, iterable)函數(shù)的功能是遍歷列表,如果以元素作為參數(shù)調(diào)用function時返回True的話則將其過濾出來,最后返回包含所有過濾出的元素的新列表。
7、簡化代碼

有了這些內(nèi)置函數(shù),你的代碼會變得更簡潔,沒有了循環(huán)體,數(shù)據(jù)集,操作,返回值都放到了一起。特別是用了reduce()以后,連for,while循環(huán)都省了。

再看個例子,我們有3輛車比賽,簡單起見,我們分別給這3輛車有70%的概率可以往前走一步,一共有5次機(jī)會,我們打出每一次這3輛車的前行狀態(tài)。用指令式編程的代碼如下:

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]

如果改用函數(shù)式或稱指令式編程,則是這樣的:

from random import random
L = [0]*3
reduce(lambda ll,_: map(lambda x:(x+1) if random() > 0.3 else x, ll), range(5), L)

再看看用FP模擬Unix下的echo命令:

def monadic_print(x):
print x
return x

echo_FP = lambda: monadic_print(raw_input("FP -- "))=='quit' or echo_FP()
echo_FP()

8、偏函數(shù)(Partial function)與柯里化(Currying)

柯里化(Currying)技術(shù)是把接受多個參數(shù)的函數(shù)變換成只接受部分參數(shù)(比如原函數(shù)的第一個參數(shù))的函數(shù),并且返回接受余下的參數(shù)的新函數(shù),新函數(shù)稱為偏函數(shù)。

形如:f(x,y) ==> f(x)(y)

Python不像Scala語言那樣支持Currying。然而稍作變通即可達(dá)到生成偏函數(shù)的效果:

def add(x, y):
return x + y

def add_to(n):
return lambda x: add(n, x)

assert add(3, 2) == add_to(3)(2)

但Python內(nèi)置的functools模塊提供了一個函數(shù)partial,可以為任意函數(shù)生成偏函數(shù):

functools.partial(func[, *args][, **keywords])

import functools
f3 = functools.partial(add, 3)
assert add(3, 2) == f3(2)

9、閉包(Closure)

如果一個函數(shù)定義在另一個函數(shù)的作用域內(nèi),并且引用了外層函數(shù)的變量,則該函數(shù)稱為閉包。下例中inner()就是一個閉包,本身是一個函數(shù),而且可以訪問(在python2.x中是只讀的)本身之外的變量n。

def f():
n = 1
def inner():
print n
return inner

f()()

10、函數(shù)裝飾器(Decorator)

Python的Decorator(函數(shù)裝飾器)在功能上類似Java的函數(shù)注解(Annotation)。它首先是個閉包,存放了fn及自定義的變量。然后再返回一個wrapper函數(shù),真正對fn及fn的參數(shù)進(jìn)行AOP處理。如果要使用帶參數(shù)的decorator還需要多包裹一層,先返回一個保存著decorator參數(shù)的閉包,再返回一個保存了fn的閉包。

看一個通俗的計算函數(shù)運行時間的例子,其中對于運行時間的統(tǒng)計與原功能做到了分離:

import time

def timeit(func):
def wrapper():
start = time.clock()
func()
end =time.clock()
print 'used:', end - start
return wrapper

@timeit
def foo():
print 'in foo()'

foo()

再看一個計算斐波那契數(shù)列的例子,每次遞歸都會有重復(fù)計算,如果能講中間結(jié)果記錄下來就可以提高性能。(增加了可選的 @warps 是為了避免一些副作用,比如func.name等屬性保持為原函數(shù)的名字而非wrapper,防止采用反射時遇到問題。)

from functools import wraps
def memo(fn):
cache = {}
miss = object()

@wraps(fn)
def wrapper(*args, **kwargs):
    result = cache.get(args, miss)
    if result is miss:
        result = fn(*args)
        cache[args] = result
    return result

return wrapper

@memo
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)

實質(zhì)上,@decorator寫法其實是高階函數(shù)的語法糖,每一次裝飾都生成了一個新的函數(shù)。下例中的兩種寫法是等價的:

@decorator_one
@decorator_two
def fn():
pass

func = decorator_one(decorator_two(fn))

===================

@decorator_one(arg1, arg2)
@decorator_two
def fn(param1):
pass

func = decorator_one(arg1, arg2) (decorator_two (fn) )

11、遞歸(Recursion)

在FP中,通常通過遞歸來實現(xiàn)循環(huán)。遞歸函數(shù)會不斷調(diào)用自身,直到到達(dá)最基本的條件。

看個用線性遞歸代替循環(huán)來求和的例子(從1…5循環(huán)):

def lsum(f, a, b):
if (a>b):
return 0
else:
return f(a)+lsum(f, a+1, b)

print lsum(lambda x:x, 1, 5)

12、尾遞歸(Tail Recursion)

由于每次線性遞歸(Linear Recursive)調(diào)用都需要維護(hù)一個棧(stack),來保存臨時狀態(tài),因此大量遞歸會帶來性能問題,但是利用尾遞歸(Tail Recursive)可以進(jìn)行優(yōu)化,每次遞歸通過傳參的方式來傳遞狀態(tài),減少stack占用。如果編譯器支持的話,還可以將遞歸形式展開優(yōu)化為while循環(huán)的形式(目前Python編譯器暫不支持該優(yōu)化)。下例中變量acc在每次遞歸后都會將最新狀態(tài)帶入下一次遞歸。

def tsum(f, a, b):
def loop(a, acc):
if a>b:
return acc
else:
return loop(a+1, acc+f(a))
return loop(a,0)

print tsum(lambda x:x, 1, 5)

13、嚴(yán)格與非嚴(yán)格求值(Strict vs. non-strict evaluation)

FP語言可以分為嚴(yán)格(及早)求值與非嚴(yán)格(惰性)求值,區(qū)別在于對表達(dá)式求值的時機(jī)。看下面這個例子:

print len([2+1, 3*2, 1/0, 5-4])

在Python中執(zhí)行上述語句會報錯,因為以0為除數(shù)是非法的。可以看出Python對于數(shù)值運算是嚴(yán)格求值的,而像Haskell的默認(rèn)方式就是非嚴(yán)格求值,因而上述語句的執(zhí)行結(jié)果就是4,即列表的長度。

而Python中也存在惰性求值的語法,比如相對于range(n)函數(shù),xrange(n)是其惰性版本。

再如相對于列表生成器[x+1 for x in range(5)],惰性版本可以寫成(x+1 for x in range(5))。你可以print一下,看看兩者的區(qū)別。
14、迭代器 (Iterator)

只要實現(xiàn)了next()函數(shù)的類,都可成為迭代器,每次調(diào)用next()函數(shù),就應(yīng)當(dāng)返回序列中的下一個值。內(nèi)置的數(shù)據(jù)結(jié)構(gòu)如tuple、list、dict、set等都已經(jīng)實現(xiàn)了迭代器。

對于列表,for循環(huán)通常是以遍歷迭代器的形式,比如要從1~5循環(huán),可以寫成:

for i in [1,2,3,4,5]:
pass

對于列表,如果還想獲得循環(huán)的索引,可以這樣寫:

for i, index in enumerate([1,2,3,4,5]):
pass

對于字典,可以這樣遍歷:

for k, v in {'a':1, 'b':2}.items():
pass

內(nèi)置的itertools庫提供了更有效更豐富的迭代器,包括去重、笛卡爾積、無限迭代、條件迭代。同時還有各種內(nèi)置函數(shù)的惰性版本,比如相對于map()的imap()等。

開源庫Fn.py庫也實現(xiàn)了無限序列等。
15、列表生成器的解析

列表生成器可用來快速生成列表,可以代替map()或filter()的使用。(注意例子中用的是嚴(yán)格求值的方式,否則還需要一次遍歷才能展開列表)

比如下例中的寫法是等價的:

[x+1 for x in range(5)]
map(lambda x:x+1, range(5))

[x for x in range(10) if x%2==0]
filter(lambda x:x%2==0, range(10))

如果是多重循環(huán)解析,則可以寫成:(注意例子中可以用惰性求值的方式)

((x, y) for x in range(3) for y in range(x))

如果是組合循環(huán)解析,則可以寫成:(注意例子中可以用惰性求值的方式)

(x for x in (y.doSomething() for y in lst) if x>0)

16、生成器(Generator)

生成器是一個特殊的迭代器,需要用到y(tǒng)ield關(guān)鍵字。包含該關(guān)鍵字的函數(shù)會自動成為一個生成器對象。里面的代碼一般是一個有限或無限循環(huán)結(jié)構(gòu),每當(dāng)調(diào)用該函數(shù)時,會執(zhí)行到y(tǒng)ield代碼為止并返回本次迭代結(jié)果。然后凍結(jié)(freeze)在這一行,直到外部調(diào)用者的下一次調(diào)用該函數(shù)時,再返回下一次迭代結(jié)果。通過這種方式,迭代器可以實現(xiàn)惰性求值。

看一個用生成器來計算斐波那契數(shù)列的例子。其中求值函數(shù)是一個無限循環(huán)的生成器,而外部調(diào)用該生成器時,需要顯式地控制迭代次數(shù)。

def fibonacci():
a = b = 1
yield a
yield b
while True:
a, b = b, a+b
yield b

for num in fibonacci():
if num > 100: break
print num,

另一例是牛頓法開平方根,而每次迭代都會更加逼近真實值。下例是生成器與尾遞歸兩種寫法的比較,其中生成器內(nèi)是一個有條件循環(huán)。(”_”是合法變量名,用作變量占位符,最后一行的reduce相當(dāng)于for循環(huán)的作用)

def square(k):
guess=1
yield guess
while abs(guess*guess-k)>0.001:
guess=(guess+k/guess)/2.0
yield guess
return
for z in square(2):
print z

def improve(guess, k):
return (guess+k/guess)/2.0
print reduce(lambda guess, _: improve(guess, 2), range(4), 1)

再看一個中序(inorder)遍歷二叉樹的例子,里面用到了生成器的遞歸嵌套:

class Tree:
def init(self,left, label, right) :
self.left = left;
self.label = label;
self.right = right;

def inorder(t):
if t is not None:
for x in inorder(t.left): yield x
yield t.label
for x in inorder(t.right): yield x

def make_tree(array):
if len(array) == 1:
return Tree(None, array[0], None)
return Tree(make_tree(array[0]), array[1], make(array[2]))

tree = make_tree([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]])
print [n for n in inorder(tree)]

17、管道式表達(dá)式

函數(shù)嵌套調(diào)用,看起來沒有那么清爽。如果能將數(shù)據(jù)看成流,函數(shù)之間像shell里面的管道一樣來傳遞數(shù)據(jù),結(jié)果會更清楚一些。下段代碼示例揭示了其中的原理,通過在裝飾器中重載ror運算符(從右向左進(jìn)行”或”操作)并且返回一個迭代器來做到這一點,于是,函數(shù)也就可以通過|運算符來互相操作了。

class Pipe(object):
def init(self, func):
self.func = func

def __ror__(self, other):
    def generator():
        for obj in other:
            if obj is not None:
                yield self.func(obj)
    return generator()

@Pipe
def even_filter(num):
return num if num % 2 == 0 else None

@Pipe
def multiply_by_three(num):
return num*3

@Pipe
def convert_to_string(num):
return 'The Number: %s' % num

@Pipe
def echo(item):
print item
return item

def force(sqs):
for item in sqs: pass

nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
force(nums | even_filter | multiply_by_three | convert_to_string | echo)

這正是開源庫pipe.py所實現(xiàn)的管道式調(diào)用/流式操作。這種方式也比較適合參數(shù)校驗、判空等場景。pipe.py的用法更加簡潔一些:

from pipe import *

range(5) | add

fibonacci() | where(lambda x: x % 2 == 0) | take_while(lambda x: x < 10000) | add

@Pipe
def take_while_idx(iterable, predicate):
for idx, x in enumerate(iterable):
if predicate(idx): yield x
else: return

fibonacci() | take_while_idx(lambda x: x < 10) | as_list

用pipe和itertools重新實現(xiàn)一下之前的賽車題目:

from random import random
from pipe import *
import itertools

print itertools.count(1) | take(5) | aggregate(lambda ll,_: ll | select(lambda x:(x+1) if random() > 0.3 else x), initializer=[0]*3) | as_list

18、Refer:

[1] Python函數(shù)式編程

http://www.jackyshen.com/2014/10/02/functional-programming-in-Python/

[2] Python函數(shù)式編程指南:目錄和參考

http://www.cnblogs.com/huxi/archive/2011/07/15/2107536.html

[3] Fn.py:享受Python中的函數(shù)式編程

http://www.infoq.com/cn/articles/fn.py-functional-programming-python

[4] 函數(shù)式編程

http://coolshell.cn/articles/10822.html

[5] Python修飾器的函數(shù)式編程

http://coolshell.cn/articles/11265.html

[6] 可愛的 Python : Python中函數(shù)式編程,第一部分

http://www.oschina.net/translate/python-functional-programming-part1

[7] 尾遞歸

http://baike.baidu.com/view/1439396.htm
原文地址:http://www.jackyshen.com/2014/10/02/functional-programming-in-Python/

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

推薦閱讀更多精彩內(nèi)容