Python函数式编程:从入门到走火入魔
shl198001
8年前
<p>很多人都在谈论函数式编程(Functional Programming),只是很多人站在不同的角度看到的是完全不一样的风景。坚持实用主义的 Python 老司机们对待 FP 的态度应该更加包容,虽然他们不相信银弹,但冥冥中似乎能感觉到 FP 暗合了 Python 教义(The Zen of Python)的某些思想,而且既然 Python 是一门多范式编程语言,并在很大程度上支持函数式编程,那就更没有理由拒绝它。</p> <p>函数式编程源自于数学理论,它似乎也更适用于数学计算相关的场景,因此本文以一个简单的数据处理问题为例,逐步介绍 Python 函数式编程从入门到走火入魔的过程。</p> <p>问题:计算 N 位同学在某份试卷的 M 道选择题上的得分(每道题目的分值不同)。</p> <p>首先来生成一组用于计算的伪造数据:</p> <pre> <code class="language-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', ...</code></pre> <h3><strong>入门</strong></h3> <p>首先来看常规的面向过程编程风格,我们需要遍历每个学生,然后遍历每个学生对每道题目的答案并与真实答案进行比较,然后将正确答案的分数累计:</p> <pre> <code class="language-python">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 ... """</code></pre> <p>如果你觉得上面的代码非常直观且合乎逻辑,那说明你已经习惯按照计算机的思维模式进行思考了。通过创建嵌套两个 for 循环来 <strong>遍历</strong> 所有题目答案的判断和评分,这完全是为计算机服务的思路,虽然说 Python 中的 for 循环已经比 C 语言更进了一步,通常不需要额外的状态变量来记录当前循环的次数,但有时候也不得不使用状态变量,如上例中第二个循环中比较两个列表的元素。函数式编程的一大特点就是尽量抛弃这种明显 <strong>循环遍历</strong> 的做法,而是把注意集中在解决问题本身,一如在现实中我们批改试卷时,只需要将两组答案 <strong>并列</strong> 进行比较即可:</p> <pre> <code class="language-python">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)), ...]</code></pre> <p>然后再将所有正确题目的分数累加起来,即可:</p> <pre> <code class="language-python">from functools import reduce reduced = reduce(lambda x, y: x + y[1][1], filtered, 0) print(reduced)</code></pre> <p>以上是对一位学生的结果处理,接下来只需要对所有学生进行同样的处理即可:</p> <pre> <code class="language-python">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 ... """</code></pre> <p>上面的示例通过 zip/filter/reduce/map 等函数将数据处理的方法 <strong>打包</strong> 应用到数据上,实现了基本的函数式编程操作。但是如果你对函数式有更深入的了解,你就会发现上面的 cal 方法中使用了全局变量 QUIZE ,这会导致在相同输入的条件下,函数可能产生不同的输出,这是 FP 的大忌,因此需要进行整改:</p> <pre> <code class="language-python">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)</code></pre> <p>如此借助闭包(Closure)的方法,就可以维持纯净的 FP 模式啦!</p> <h3><strong>走火(fn.py)</strong></h3> <p>也许看了上面的 FP 写法,你还是觉得挺啰嗦的,并没有达到你想象中的结果,这时候就需要呈上一款语法糖利器: <a href="/misc/goto?guid=4959550647511506150" rel="nofollow,noindex">fn.py</a> ! fn.py 封装了一些常用的 FP 函数及语法糖,可以大大简化你的代码!</p> <pre> <code class="language-python">pip install fn</code></pre> <p>首先从刚刚的闭包开始,我们可以用更加 FP 的方法来解决这一问题,称为 <a href="/misc/goto?guid=4958965029775868658" rel="nofollow,noindex">柯里化</a> ,简单来说就是 允许接受多个参数的函数可以分次执行,每次只接受一个参数 :</p> <pre> <code class="language-python">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</code></pre> <p>应用到上面的 cal 方法中:</p> <pre> <code class="language-python">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)</code></pre> <p>在 FP 中数据通常被看作是一段数据流在一串函数的管道中传递,因此上面的 reduce 和 filter 其实可以合并:</p> <pre> <code class="language-python">reduce(lambda x, y: x + y[1][1], filter(lambda x: x[0] == x[1][0], zip(student.ans, quize)), 0)</code></pre> <p>虽然更简略了,但是这样会大大降低代码的可读性(这也是 FP 容易遭受批评的一点),为此 fn 提供了更高级的函数操作工具:</p> <pre> <code class="language-python">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)</code></pre> <h3><strong>入魔(Hy)</strong></h3> <p>如果你觉得上面的代码已经足够魔性到看起来不像是 Python 语言了,然而一旦接受了这样的语法设定感觉也还挺不错的。如果你兴冲冲地拿去给 Lisp 或 Haskell 程序员看,则一定会被无情地鄙视:joy:,于是你痛定思痛下定决心继续挖掘 Python 函数式编程的奥妙,那么恭喜你,组织欢迎你的加入: Hail Hydra !</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/67a2bbaaf473f4a951e57de8293133ea.jpg"></p> <p>哦不对,说漏了,是 Hi Hy !</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/feefd6b6af3dd98b602d886f636dc845.jpg"></p> <p>Hy 是基于 Python 的 Lisp 方言,可以与 Python 代码进行完美互嵌(如果你更偏好 PyPy,同样也有类似的 <a href="/misc/goto?guid=4959727828639770866" rel="nofollow,noindex">Pixie</a> ),除此之外你也可以把它当做一门独立的语言来看待,它有自己的解释器,可以当做独立的脚本语言来使用:</p> <pre> <code class="language-python">pip install git+https://github.com/hylang/hy.git</code></pre> <p>首先来看一下它的基本用法,和 Python 一样,安装完之后可以通过 hy 命令进入 REPL 环境:</p> <pre> <code class="language-python">=> (print "Hy!") Hy! => (defn salutationsnm [name] (print (+ "Hy " name "!"))) => (salutationsnm "YourName") Hy YourName!</code></pre> <p>或者当做命令行脚本运行:</p> <pre> <code class="language-python">#! /usr/bin/env hy (print "I was going to code in Python syntax, but then I got Hy.")</code></pre> <p>保存为 awesome.hy :</p> <pre> <code class="language-python">chmod +x awesome.hy ./awesome.hy</code></pre> <p>接下来继续以上面的问题为例,首先可以直接从 Python 代码中导入:</p> <pre> <code class="language-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))</code></pre> <p>如果觉得不放心,还可以直接调用最开始定义的方法将结果进行比较:</p> <pre> <code class="language-python">;; 假设最上面的 normal 方法保存在 fun.py 文件中 (import fun) (.normal fun students quize)</code></pre> <h3><strong>总结</strong></h3> <p>以一个简单的数据处理问题为例,我们经历了 Python 函数式编程从开始尝试到“走火入魔”的整个过程。也许你还是觉得不够过瘾,想要尝试更 <strong>纯粹</strong> 的 FP 体验,那么 Haskell 将是你最好的选择。FP 将数据看做数据流在不同函数间传递,省去不必要的中间变量,保证函数的纯粹性…等等这些思想在数据处理过程中还是非常有帮助的(Python 在这一领域的竞争对手 R 语言本身在语法设计上就更多地受到 Lisp 语言的影响,虽然看起来语法也比较奇怪,但这也是它比较适合用于数据处理及统计分析的原因之一)。</p> <h3><strong>参考</strong></h3> <ol> <li><a href="/misc/goto?guid=4959727828728591263" rel="nofollow,noindex">Tips » 0x02-函数式编程</a></li> <li><a href="/misc/goto?guid=4959727828810532872" rel="nofollow,noindex">Python HOWTOs » Functional Programming HOWTO</a></li> <li><a href="/misc/goto?guid=4959727828881103412" rel="nofollow,noindex">Hy's Doc</a></li> <li><a href="/misc/goto?guid=4959550647511506150" rel="nofollow,noindex">fn.py</a></li> </ol> <p> </p> <p>来自:http://blog.rainy.im/2016/12/04/fp-with-py/</p> <p> </p>