写点什么

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

2013 年 3 月 12 日

尽管 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 )关注我们,并与我们的编辑和其他读者朋友交流。

2013 年 3 月 12 日 06:5012017
用户头像

发布了 428 篇内容, 共 148.4 次阅读, 收获喜欢 20 次。

关注

评论

发布
暂无评论
发现更多内容

NIO 看破也说破(三)—— 不同的IO模型

小眼睛聊技术

Java 学习 深度思考 程序员 架构

每个人都应该知道的性能参数

ElvinYang

C语言常量、变量和关键字

C语言技术网-码农有道

C语言 常量 变量 关键字

追光逐影:读《我们这一代》

北风

对话 CTO | 听快看漫画 CTO 李润超讲重塑漫画产业的技术推动力

ONES 王颖奇

研发管理 CTO 动画 文化

给应届毕业生们的七点建议

Neco.W

大学生日常 工作 应届毕业

如何让团队产生“多米诺骨牌”效应?

Yanel 说敏捷产品

项目管理 敏捷 敏捷开发 敏捷精髓

工具集系列 02|还在为海报设计、LOGO 设计发愁?这些在线工具值得收藏

一尘观世界

效率工具 设计 海报 课程封面 知识付费

C语言输入和输出

C语言技术网-码农有道

C语言 输入 输出

对话 CTO | 喜茶也有 CTO?听陈霈霖讲讲茶饮中的技术甜度

ONES 王颖奇

研发管理 CTO 零售

前端有未来吗?

欧雷

前端 前端开发

Using R for everything: 方差分解(Variation partition)变量筛选与显著性标注

洗衣机用户不会用洗衣机

数据分析 R

从技术层面理解对于区块链技术的10.24集体学习讲话

MaxHu

区块链 智能合约 以太坊 加密货币 去中心化网络

C语言运算符

C语言技术网-码农有道

C语言 运算符

“随大流”的你是不会成功的

小天同学

个人成长 思考 写作平台 感悟 坚持

你真的懂"看板文化"么?

Yanel 说敏捷产品

敏捷 敏捷开发 敏捷精髓

认识数据产品经理(二 数据产品经理的稀缺性)

马踏飞机747

大数据 互联网 数据分析 产品经理

目光聚集之处,金钱必将追随

Tom

学习 个人成长 思考 读书

危机过后,「表格文档协同」需要具备什么能力?

Geek_Willie

前端开发 开发者工具 Excel

Python网络编程socket 简易聊天窗

Flychen

如何高效阅读

ElvinYang

接口限流算法有哪些,看完这篇又能和面试官互扯了~

不才陈某

Java 分布式 后端

DDD 实践手册(6. Bounded Context - 限界上下文)

Joshua

企业架构 设计模式 领域驱动设计 DDD 架构模式

工具集系列|值得收藏的几个免费在线学习国外网站

一尘观世界

学习 工具 网站 提升

Linux学习-2020.05.11

Flychen

【解析+示例】2种方法,通过SpreadJS在前端实现甘特图

Geek_Willie

前端开发 甘特图 SpreadJS 表格控件

当前的经济形势,如何让自己免于风险?

鼎玉谷

Try-Catch包裹的代码异常后,竟然导致了产线事务回滚!

码大叔

Java spring 事务

ShedLock:一个轻量级的定时任务协调组件

kk

定时任务 shedlock

Python程序性能分析和火焰图

ElvinYang

JavaScript 学习笔记——数据类型

zjlulsum

Java 学习 前端 类型推断 入门

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

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