利用fn.py库在python中进行函数式编程

尽管python事实上并不是一门纯函数式编程语言,但它本身是一门多范型语言,并给了你足够的自由利用函数式编程的便利。函数式风格有着各种理论与实际上的好处(你可以在python的文档中找到这个列表):

形式上可证
模块性
组合性
易于调试及测试

虽然这份列表已经描述得够清楚了,但我还是很喜欢michael o.church在他的文章“函数式程序极少腐坏(functional programs rarely rot)”中对函数式编程的优点所作的描述。我在pycon ua 2012期间的讲座“functional programming with python”中谈论了在python中使用函数式方式的内容。我也提到,在你尝试在python中编写可读同时又可维护的函数式代码时,你会很快发现诸多问题。

fn.py类库就是为了应对这些问题而诞生的。尽管它不可能解决所有问题,但对于希望从函数式编程方式中获取最大价值的开发者而言,它是一块“电池”,即使是在命令式方式占主导地位的程序中,也能够发挥作用。那么,它里面都有些什么呢?
scala风格的lambda定义

在python中创建lambda函数的语法非常冗长,来比较一下:

python

map(lambda x: x*2, [1,2,3])

scala

代码如下:

list(1,2,3).map(_*2)

clojure

代码如下:

(map #(* % 2) ‘(1 2 3))

haskell

代码如下:

map (2*) [1,2,3]

受scala的启发,fn.py提供了一个特别的_对象以简化lambda语法。

from fn import _
assert (_ + _)(10, 5) = 15
assert list(map(_ * 2, range(5))) == [0,2,4,6,8]
assert list(filter(_ < 10, [9,10,11])) == [9]

除此之外还有许多场景可以使用_:所有的算术操作、属性解析、方法调用及分片算法。如果你不确定你的函数具体会做些什么,你可以将结果打印出来:

from fn import _
print (_ + 2) # “(x1) => (x1 + 2)”
print (_ + _ * _) # “(x1, x2, x3) => (x1 + (x2 * x3))”

流(stream)及无限序列的声明

scala风格的惰性求值(lazy-evaluated)流。其基本思路是:对每个新元素“按需”取值,并在所创建的全部迭代中共享计算出的元素值。stream对象支持 import sys
>>> fact(sys.getrecursionlimit() * 2)
… many many lines of stacktrace …
runtimeerror: maximum recursion depth exceeded

这也是件好事,至少它避免了在你的代码中产生严重错误。

我们如何优化这个方案呢?答案很简单,只需改变函数以使用尾递归即可:

def fact(n, acc=1):
if n == 0: return acc
return fact(n-1, acc*n)

为什么这种方式更佳呢?因为你不需要保留之前的值以计算出最终结果。可以在wikipedia上查看更多尾递归调用优化的内容。可是……python的解释器会用和之前函数相同的方式执行这段函数,结果是你没得到任何优化。

fn.recur.tco为你提供了一种机制,使你可以使用“蹦床”方式获得一定的尾递归优化。同样的方式也使用在诸如clojure语言中,主要思路是将函数调用序列转换为while循环。

from fn import recur
@recur.tco
def fact(n, acc=1):
if n == 0: return false, acc
return true, (n-1, acc*n)

@recur.tco是一个修饰符,能将你的函数执行转为while循环并检验其输出内容:

(false, result)代表运行完毕
(true, args, kwargs)代表我们要继续调用函数并传递不同的参数
(func, args, kwargs)代表在while循环中切换要执行的函数

函数式风格的错误处理

假设你有一个request类,可以按照传入其中的参数名称得到对应的值。要想让其返回值格式为全大写、非空并且去除头尾空格的字符串,你需要这样写:

class request(dict):
def parameter(self, name):
return self.get(name, none)
r = request(testing=”fixed”, empty=” “)
param = r.parameter(“testing”)
if param is none:
fixed = “”
else:
param = param.strip()
if len(param) == 0:
fixed = “”
else:
fixed = param.upper()

额,看上去有些古怪。用fn.monad.option来修改你的代码吧,它代表了可选值,每个option实例可代表一个full或者empty(这点也受到了scala中option的启发)。它为你编写长运算序列提供了简便的方法,并且去掉除了许多if/else语句块。

from operator import methodcaller
from fn.monad import optionable
class request(dict):
@optionable
def parameter(self, name):
return self.get(name, none)
r = request(testing=”fixed”, empty=” “)
fixed = r.parameter(“testing”)
.map(methodcaller(“strip”))
.filter(len)
.map(methodcaller(“upper”))
.get_or(“”)

fn.monad.option.or_call是个便利的方法,它允许你进行多次调用尝试以完成计算。例如,你有一个request类,它有type,mimetype和url等几个可选属性,你需要使用最少一个属性值以分析它的“request类型”:

from fn.monad import option
request = dict(url=”face.png”, mimetype=”png”)
tp = option \
.from_value(request.get(“type”, none)) \ # check “type” key first
.or_call(from_mimetype, request) \ # or.. check “mimetype” key
.or_call(from_extension, request) \ # or… get “url” and check extension
.get_or(“application/undefined”)

其余事项?

我仅仅描述了类库的一小部分,你还能够找到并使用以下功能:

22个附加的itertools代码段,以扩展内置module的功能的附加功能
将python 2和python 3的迭代器(iterator)(如range,map及filtter等等)使用进行了统一,这对使用跨版本的类库时非常有用
为函数式组合及partial函数应用提供了简便的语法
为使用高阶函数(apply,flip等等)提供了附加的操作符

正在进行中的工作

自从在github上发布这个类库以来,我从社区中收到了许多审校观点、意见和建议,以及补丁和修复。我也在继续增强现有功能,并提供新的特性。近期的路线图包括以下内容:

为使用可迭代对象(iterable),如foldl,foldr增加更多操作符
更多的monad,如fn.monad.either,以处理错误记录
为大多数module提供c-accelerator
为简化lambda arg1: lambda arg2:…形式而提供的curry函数的生成器
更多文档,更多测试,更多示例代码

Posted in 未分类

发表评论