Fn.py:享受 Python 中的函数式编程

  • 2013-03-11
  • 本文字数:3362 字

    阅读完需:约 11 分钟

尽管 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 对象支持 << 操作符,代表在需要时将新元素推入其中。

惰性求值流对无限序列的处理是一个强大的抽象。我们来看看在函数式编程语言中如何计算一个斐波那契序列。

Haskell


fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

Clojure


(def fib (lazy-cat [0 1] (map + fib (rest fib))))

Scala


def fibs: Stream[Int] = 
    0 #:: 1 #:: fibs.zip(fibs.tail).map{case (a,b) => a + b} 

现在你可以在 Python 中使用同样的方式了:


from fn import Stream 
from fn.iters import take, drop, map
from operator import add

f = Stream()
fib = f << [0, 1] << map(add, f, drop(1, f))

assert list(take(10, fib)) == [0,1,1,2,3,5,8,13,21,34]
assert fib[20] == 6765
assert list(fib[30:35]) == [832040,1346269,2178309,3524578,5702887]

蹦床(Trampolines)修饰符

fn.recur.tco 是一个不需要大量栈空间分配就可以处理 TCO 的临时方案。让我们先从一个递归阶乘计算示例开始:


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

这种方式也能工作,但实现非常糟糕。为什么呢?因为它会递归式地保存之前的计算值以算出最终结果,因此消耗了大量的存储空间。如果你对一个很大的 n 值(超过了 sys.getrecursionlimit() 的值)执行这个函数,CPython 就会以此方式失败中止:


>>> 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 函数的生成器
  • 更多文档,更多测试,更多示例代码

链接

如果你想了解这个类库的更多信息,可以使用以下资源:

关于作者

Alexey Kachayev是一个精力充沛且狂热的程序员,开源社区的活跃者,并经常在各种技术会议中进行演讲。他是 Kitapps Inc 的 CTO。Alexey 在 Python、Erlang、Clojure 及函数式编程语言(如 Haskel 及 Lisp)等方面经验最丰富。他主要的兴趣所在是分布式应用、云计算、实时 web 和编译原理等。Alexey 也为 CPython 解释器和 Storm(实时数据处理器)贡献过自己的力量。

查看英文原文 Fn.py: Enjoy Functional Programming in Python


感谢杨赛对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。