不少人都在談論函數式編程(Functional Programming),只是不少人站在不一樣的角度看到的是徹底不同的風景。堅持實用主義的 Python 老司機們對待 FP 的態度應該更加包容,雖然他們不相信銀彈,但冥冥中彷佛能感受到 FP 暗合了 Python 教義(The Zen of Python)的某些思想,並且既然 Python 是一門多範式編程語言,並在很大程度上支持函數式編程,那就更沒有理由拒絕它。javascript
函數式編程源自於數學理論,它彷佛也更適用於數學計算相關的場景,所以本文以一個簡單的數據處理問題爲例,逐步介紹 Python 函數式編程從入門到走火入魔的過程。html
問題:計算 N 位同窗在某份試卷的 M 道選擇題上的得分(每道題目的分值不一樣)。java
首先來生成一組用於計算的僞造數據:python
# @file: data.py
import random
from collections import namedtuple
Student = namedtuple('Student', ['id', 'ans'])
N_Questions = 25
N_Students = 20
def gen_random_list(opts, n):
return [random.choice(opts) for i in range(n)]
# 問題答案 'ABCD' 隨機
ANS = gen_random_list('ABCD', N_Questions)
# 題目分值 1~5 分
SCORE = gen_random_list(range(1,6), N_Questions)
QUIZE = zip(ANS, SCORE)
students = [
# 學生答案爲 'ABCD*' 隨機,'*' 表明未做答
Student(_id, gen_random_list('ABCD*', N_Questions))
for _id in range(1, N_Students+1)
]
print(QUIZE)
# [('A', 3), ('B', 1), ('D', 1), ...
print(students)
# [Student(id=1, ans=['C', 'B', 'A', ...複製代碼
首先來看常規的面向過程編程風格,咱們須要遍歷每一個學生,而後遍歷每一個學生對每道題目的答案並與真實答案進行比較,而後將正確答案的分數累計:git
import data
def normal(students, quize):
for student in students:
sid = student.id
score = 0
for i in range(len(quize)):
if quize[i][0] == student.ans[i]:
score += quize[i][1]
print(sid, '\t', score)
print('ID\tScore\n==================')
normal(data.students, data.quize)
""" ID Score ================== 1 5 2 12 ... """複製代碼
若是你以爲上面的代碼很是直觀且合乎邏輯,那說明你已經習慣按照計算機的思惟模式進行思考了。經過建立嵌套兩個 for
循環來遍歷全部題目答案的判斷和評分,這徹底是爲計算機服務的思路,雖說 Python 中的 for
循環已經比 C
語言更進了一步,一般不須要額外的狀態變量來記錄當前循環的次數,但有時候也不得不使用狀態變量,如上例中第二個循環中比較兩個列表的元素。函數式編程的一大特色就是儘可能拋棄這種明顯循環遍歷的作法,而是把注意集中在解決問題自己,一如在現實中咱們批改試卷時,只須要將兩組答案並列進行比較便可:程序員
from data import students, QUIZE
student = students[0]
# 將學生答案與正確答案合併到一塊兒
# 而後過濾出答案一致的題目
filtered = filter(lambda x: x[0] == x[1][0], zip(student.ans, QUIZE))
print(list(filtered))
# [('A', ('A', 3)), ('D', ('D', 1)), ...]複製代碼
而後再將全部正確題目的分數累加起來,便可:github
from functools import reduce
reduced = reduce(lambda x, y: x + y[1][1], filtered, 0)
print(reduced)複製代碼
以上是對一位學生的結果處理,接下來只須要對全部學生進行一樣的處理便可:編程
def cal(student):
filtered = filter(lambda x: x[0] == x[1][0], zip(student.ans, QUIZE))
reduced = reduce(lambda x, y: x + y[1][1], filtered, 0)
print(student.id, '\t', reduced)
print('ID\tScore\n==================')
# 因爲 Python 3 中 map 方法只是組合而不直接執行
# 須要轉換成 list 才能將 cal 方法的的結果打印出來
list(map(cal, students))
""" ID Score ================== 1 5 2 12 ... """複製代碼
上面的示例經過 zip/filter/reduce/map
等函數將數據處理的方法打包應用到數據上,實現了基本的函數式編程操做。可是若是你對函數式有更深刻的瞭解,你就會發現上面的 cal
方法中使用了全局變量 QUIZE
,這會致使在相同輸入的條件下,函數可能產生不一樣的輸出,這是 FP 的大忌,所以須要進行整改:閉包
def cal(quize):
def inner(student):
filtered = filter(lambda x: x[0] == x[1][0], zip(student.ans, quize))
reduced = reduce(lambda x, y: x + y[1][1], filtered, 0)
print(student.id, '\t', reduced)
return inner
map(cal(QUIZE), students)複製代碼
如此藉助閉包(Closure)的方法,就能夠維持純淨的 FP 模式啦!dom
也許看了上面的 FP 寫法,你仍是以爲挺囉嗦的,並無達到你想象中的結果,這時候就須要呈上一款語法糖利器:fn.py!fn.py
封裝了一些經常使用的 FP 函數及語法糖,能夠大大簡化你的代碼!
pip install fn複製代碼
首先從剛剛的閉包開始,咱們能夠用更加 FP 的方法來解決這一問題,稱爲柯里化,簡單來講就是容許接受多個參數的函數能夠分次執行,每次只接受一個參數:
from fn.func import curried
@curried
def sum5(a, b, c, d, e):
return a + b + c + d + e
sum3 = sum5(1,2)
sum4 = sum3(3,4)
print(sum4(5))
# 15複製代碼
應用到上面的 cal
方法中:
from fn.func import curried
@curried
def cal(quize, student):
filtered = filter(lambda x: x[0] == x[1][0], zip(student.ans, quize))
reduced = reduce(lambda x, y: x + y[1][1], filtered, 0)
print(student.id, '\t', reduced)
map(cal(QUIZE), students)複製代碼
在 FP 中數據一般被看做是一段數據流在一串函數的管道中傳遞,所以上面的reduce
和filter
其實能夠合併:
reduce(lambda x, y: x + y[1][1], filter(lambda x: x[0] == x[1][0], zip(student.ans, quize)), 0)複製代碼
雖然更簡略了,可是這樣會大大下降代碼的可讀性(這也是 FP 容易遭受批評的一點),爲此 fn
提供了更高級的函數操做工具:
from fn import F
cal = F() >> (filter, lambda x: x[0]==x[1][0]) >> (lambda r: reduce(_+_[1][1], r, 0))
# 計算一名學生的成績
print(cal(zip(student.ans, QUIZE)))
# 而後組合一下
@curried
def output(quize, student):
cal = F() >> (filter, lambda x: x[0]==x[1][0]) >> (lambda r: reduce(_+_[1][1], r, 0))
print(student.id, '\t', cal(zip(student.ans, quize)))
map(output(QUIZE), students)複製代碼
若是你以爲上面的代碼已經足夠魔性到看起來不像是 Python 語言了,然而一旦接受了這樣的語法設定感受也還挺不錯的。若是你興沖沖地拿去給 Lisp 或 Haskell 程序員看,則必定會被無情地鄙視😂,因而你痛定思痛下定決心繼續挖掘 Python 函數式編程的奧妙,那麼恭喜你,組織歡迎你的加入:Hail Hydra
!
哦不對,說漏了,是Hi Hy
!
Hy 是基於 Python 的 Lisp 方言,能夠與 Python 代碼進行完美互嵌(若是你更偏好 PyPy,一樣也有相似的Pixie),除此以外你也能夠把它當作一門獨立的語言來看待,它有本身的解釋器,能夠當作獨立的腳本語言來使用:
pip install git+https://github.com/hylang/hy.git複製代碼
首先來看一下它的基本用法,和 Python 同樣,安裝完以後能夠經過 hy
命令進入 REPL 環境:
=> (print "Hy!")
Hy!
=> (defn salutationsnm [name] (print (+ "Hy " name "!")))
=> (salutationsnm "YourName")
Hy YourName!複製代碼
或者當作命令行腳本運行:
#! /usr/bin/env hy
(print "I was going to code in Python syntax, but then I got Hy.")複製代碼
保存爲 awesome.hy
:
chmod +x awesome.hy
./awesome.hy複製代碼
接下來繼續以上面的問題爲例,首先能夠直接從 Python 代碼中導入:
(import data)
;; 用於 Debug 的自定義宏
;; 將可迭代對象轉化成列表後打印
(defmacro printlst [it]
`(print (list ~it)))
(setv students data.students)
(setv quize data.QUIZE)
(defn cal [quize]
(fn [student]
(print student.id
(reduce
(fn [x y] (+ x (last (last y))))
(filter
(fn [x] (= (first x) (first (last x))))
(zip student.ans quize))
0
)
)
)
)
(printl (map (cal quize) students))複製代碼
若是以爲不放心,還能夠直接調用最開始定義的方法將結果進行比較:
;; 假設最上面的 normal 方法保存在 fun.py 文件中
(import fun)
(.normal fun students quize)複製代碼
以一個簡單的數據處理問題爲例,咱們經歷了 Python 函數式編程從開始嘗試到「走火入魔」的整個過程。也許你仍是以爲不夠過癮,想要嘗試更純粹的 FP 體驗,那麼 Haskell 將是你最好的選擇。FP 將數據看作數據流在不一樣函數間傳遞,省去沒必要要的中間變量,保證函數的純粹性…等等這些思想在數據處理過程當中仍是很是有幫助的(Python 在這一領域的競爭對手 R 語言自己在語法設計上就更多地受到 Lisp 語言的影響,雖然看起來語法也比較奇怪,但這也是它比較適合用於數據處理及統計分析的緣由之一)。