若是程序中的函數僅接受輸入併產生輸出,即輸出只依賴於輸入,數據不可變,避免保存程序狀態,那麼就稱爲函數式編程(Functional Programming,簡稱FP,又稱泛函編程)。javascript
這種風格也稱聲明式編程(Declarative Programming),與之相對的是指令式編程(Imperative Programming),後者中的對象會不斷修改自身狀態。函數式編程強調程序的執行結果比執行過程更重要,倡導利用若干簡單的執行單元讓計算結果不斷漸進,逐層推導複雜的運算,而不是設計一個複雜的執行過程。html
函數編程語言最重要的基礎是λ演算(lambda calculus),並且λ演算的函數能夠接受函數看成輸入(引數)和輸出(傳出值)。java
函數式編程歷史悠久,最古老的例子莫過於1958年被創造出來的LISP了。而隨着程序結構複雜,面向對象編程大行其道。近年來,簡潔並且特別適合計算任務的函數式編程又從新崛起,不只僅是純粹的函數式語言如Haskell、Clojure等,各類流行語言javascripts、python、Objective-C、C#.Net甚至Java都紛紛吸取函數式編程的部分形式。並且,不只僅是計算任務,近年還出現了用FP編寫的UI應用程序,如LightTable等。python
本文做者@申導 主要採用Python語言爲例,是由於它雖然不是純粹的FP,但Python可以勝任各類編程形式,簡潔優雅,通俗易懂。特別適合從主流語言轉過來的學習者。git
純函數指的是沒有反作用(內存或I/O)的函數,函數會返回新值,而不會修改原來的值。函數間無共享變量(不像面向對象)。github
來看個非函數式的例子,它改變了變量的值。shell
int cnt; void inc() { cnt++; }
函數式的例子,不改變變量的值,而是返回一個新值。express
int cnt; int inc() { return cnt+1; }
這個特色能夠用來優化代碼。例如,一個無反作用的純函數,其執行結果具備不變性,那麼其執行結果就能夠緩存起來,供下次調用。再好比,兩個互不依賴的純函數,其執行順序能夠互換,甚至並行地執行而無需互斥。編程
在python中,不可變的元組(tuple)數據結構特別適合函數式編程。緩存
在FP中,首要特色就是將函數視爲一等公民,函數能夠當作參數來進行傳遞,造成所謂的高階函數,形如 z=g(f(x),y),還能像變量同樣被建立和修改。
這種形式在非純粹的函數式編程語言裏面多有吸取,用於簡化語法。連最古板的面嚮對象語言Java也終於在Java8中引入了lambda。
讀者若是使用過C語言,必定記得標準庫中的快排函數,其中第4個參數是一個函數指針,用於傳入一個比較(compare)函數,而排序動做被抽象成了一個模板函數。這就是一個典型的高階函數:qsort(compare(items))
void qsort(void *items, size_t nitems, size_t size, int (*compare)(const void *, const void*));
使用lambda能夠定義簡單的單行匿名函數。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)
匿名λ函數與使用def定義的函數徹底同樣,可使用lambda_add做爲函數名進行調用。然而,提供lambda的目的是爲了編寫偶爾爲之的、簡單的、可預見不會被修改的匿名函數。
考慮一個求和的例子,通常會採用循環:
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原則,上述兩段函數存在了很多重複。咱們看到除了初始值和運算符不一樣,其實總體的流程是差很少的,那麼概括(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內置的reduce(function, iterable[, initializer])函數已經實現了對列表元素依次概括的場景,而內置的all(),any(),sum(),max(),min()等函數都是基於它衍生而來。
Python內置函數還有map(function, iterable, ...)了,它抽象了另外一種情景,即遍歷列表中的每一個元素,對每一個元素執行傳入的函數,並返回包含全部新元素的新列表。
而zip(iterable1, iterable2, ...)函數則對多個列表進行合併,每一個列表的第n個元素組成一個元組(tuple),而後返回包含這些元組的新列表
filter(function, iterable)函數的功能是遍歷列表,若是以元素做爲參數調用function時返回True的話則將其過濾出來,最後返回包含全部過濾出的元素的新列表。
有了這些內置函數,你的代碼會變得更簡潔,沒有了循環體,數據集,操做,返回值都放到了一塊兒。特別是用了reduce()之後,連for,while循環都省了。
再看個例子,咱們有3輛車比賽,簡單起見,咱們分別給這3輛車有70%的機率能夠往前走一步,一共有5次機會,咱們打出每一次這3輛車的前行狀態。用指令式編程的代碼以下:
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]
若是改用函數式或稱指令式編程,則是這樣的:
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()
柯里化(Currying)技術是把接受多個參數的函數變換成只接受部分參數(好比原函數的第一個參數)的函數,而且返回接受餘下的參數的新函數,新函數稱爲偏函數。
形如:f(x,y) ==> f(x)(y)
Python不像Scala語言那樣支持Currying。然而稍做變通便可達到生成偏函數的效果:
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內置的functools模塊提供了一個函數partial,能夠爲任意函數生成偏函數:
functools.partial(func[, *args][, **keywords])
import functools f3 = functools.partial(add, 3) assert add(3, 2) == f3(2)
若是一個函數定義在另外一個函數的做用域內,而且引用了外層函數的變量,則該函數稱爲閉包。下例中inner()就是一個閉包,自己是一個函數,並且能夠訪問(在python2.x中是隻讀的)自己以外的變量n。
def f(): n = 1 def inner(): print n return inner f()()
Python的Decorator(函數裝飾器)在功能上相似Java的函數註解(Annotation)。它首先是個閉包,存放了fn及自定義的變量。而後再返回一個wrapper函數,真正對fn及fn的參數進行AOP處理。若是要使用帶參數的decorator還須要多包裹一層,先返回一個保存着decorator參數的閉包,再返回一個保存了fn的閉包。
看一個通俗的計算函數運行時間的例子,其中對於運行時間的統計與原功能作到了分離:
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()
再看一個計算斐波那契數列的例子,每次遞歸都會有重複計算,若是能講中間結果記錄下來就能夠提升性能。(增長了可選的 @warps 是爲了不一些反作用,好比func.name等屬性保持爲原函數的名字而非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)
實質上,@decorator寫法實際上是高階函數的語法糖,每一次裝飾都生成了一個新的函數。下例中的兩種寫法是等價的:
@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) )
在FP中,一般經過遞歸來實現循環。遞歸函數會不斷調用自身,直到到達最基本的條件。
看個用線性遞歸代替循環來求和的例子(從1…5循環):
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)
因爲每次線性遞歸(Linear Recursive)調用都須要維護一個棧(stack),來保存臨時狀態,所以大量遞歸會帶來性能問題,可是利用尾遞歸(Tail Recursive)能夠進行優化,每次遞歸經過傳參的方式來傳遞狀態,減小stack佔用。若是編譯器支持的話,還能夠將遞歸形式展開優化爲while循環的形式(目前Python編譯器暫不支持該優化)。下例中變量acc在每次遞歸後都會將最新狀態帶入下一次遞歸。
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)
FP語言能夠分爲嚴格(及早)求值與非嚴格(惰性)求值,區別在於對錶達式求值的時機。看下面這個例子:
print len([2+1, 3*2, 1/0, 5-4])
在Python中執行上述語句會報錯,由於以0爲除數是非法的。能夠看出Python對於數值運算是嚴格求值的,而像Haskell的默認方式就是非嚴格求值,於是上述語句的執行結果就是4,即列表的長度。
而Python中也存在惰性求值的語法,好比相對於range(n)函數,xrange(n)是其惰性版本。
再如相對於列表生成器[x+1 for x in range(5)],惰性版本能夠寫成(x+1 for x in range(5))。你能夠print一下,看看二者的區別。
只要實現了__next__()函數的類,均可成爲迭代器,每次調用next()函數,就應當返回序列中的下一個值。內置的數據結構如tuple、list、dict、set等都已經實現了迭代器。
對於列表,for循環一般是以遍歷迭代器的形式,好比要從1~5循環,能夠寫成:
for i in [1,2,3,4,5]: pass
對於列表,若是還想得到循環的索引,能夠這樣寫:
for i, index in enumerate([1,2,3,4,5]): pass
對於字典,能夠這樣遍歷:
for k, v in {'a':1, 'b':2}.items(): pass
內置的itertools庫提供了更有效更豐富的迭代器,包括去重、笛卡爾積、無限迭代、條件迭代。同時還有各類內置函數的惰性版本,好比相對於map()的imap()等。
開源庫Fn.py庫也實現了無限序列等。
列表生成器可用來快速生成列表,能夠代替map()或filter()的使用。(注意例子中用的是嚴格求值的方式,不然還須要一次遍歷才能展開列表)
好比下例中的寫法是等價的:
[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))
若是是多重循環解析,則能夠寫成:(注意例子中能夠用惰性求值的方式)
((x, y) for x in range(3) for y in range(x))
若是是組合循環解析,則能夠寫成:(注意例子中能夠用惰性求值的方式)
(x for x in (y.doSomething() for y in lst) if x>0)
生成器是一個特殊的迭代器,須要用到yield關鍵字。包含該關鍵字的函數會自動成爲一個生成器對象。裏面的代碼通常是一個有限或無限循環結構,每當調用該函數時,會執行到yield代碼爲止並返回本次迭代結果。而後凍結(freeze)在這一行,直到外部調用者的下一次調用該函數時,再返回下一次迭代結果。經過這種方式,迭代器能夠實現惰性求值。
看一個用生成器來計算斐波那契數列的例子。其中求值函數是一個無限循環的生成器,而外部調用該生成器時,須要顯式地控制迭代次數。
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,
另外一例是牛頓法開平方根,而每次迭代都會更加逼近真實值。下例是生成器與尾遞歸兩種寫法的比較,其中生成器內是一個有條件循環。(」_」是合法變量名,用做變量佔位符,最後一行的reduce至關於for循環的做用)
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)]
函數嵌套調用,看起來沒有那麼清爽。若是能將數據當作流,函數之間像shell裏面的管道同樣來傳遞數據,結果會更清楚一些。下段代碼示例揭示了其中的原理,經過在裝飾器中重載__ror__運算符(從右向左進行」或」操做)而且返回一個迭代器來作到這一點,因而,函數也就能夠經過|運算符來互相操做了。
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所實現的管道式調用/流式操做。這種方式也比較適合參數校驗、判空等場景。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從新實現一下以前的賽車題目:
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
[1] Python函數式編程
http://www.jackyshen.com/2014/10/02/functional-programming-in-Python/
[2] Python函數式編程指南:目錄和參考
http://www.cnblogs.com/huxi/archive/2011/07/15/2107536.html
[3] Fn.py:享受Python中的函數式編程
http://www.infoq.com/cn/articles/fn.py-functional-programming-python
[4] 函數式編程
http://coolshell.cn/articles/10822.html
[5] Python修飾器的函數式編程
http://coolshell.cn/articles/11265.html
[6] 可愛的 Python : Python中函數式編程,第一部分
http://www.oschina.net/translate/python-functional-programming-part1
[7] 尾遞歸
http://baike.baidu.com/view/1439396.htm
[8] 手把手介紹函數式編程:從命令式重構到函數式