来自于Python Pytoolz 函数式编程库文档的启迪

来源:互联网 发布:c语言项目开发实战 编辑:程序博客网 时间:2024/04/30 07:55

前言

首先要说明的是这是一篇节译文章。并未全部译出, 译者只是在众多的阐述函数式编程思想的文章中发现了来自Pytoolz官方文档所给予的描述最具启发性。因为它阐述简单明了,并通过实际的小例子比较透彻地说明了函数式编程的思想,对于理解和应用函数式编程,提高内功很有帮助。遂与各位python码友共享译文与此.

承启

虽然python最初的设计意图是做为一个命令式语言, 它也包含了所有必需的元素以支持来自于函数式编程语言的丰富特性。特别是它的核心数据结构, 懒迭代器(iterator), 以及函数做为一级对象, 这些特性可以合成起来实现一个拥有很多函数式编程语言所共通的标准函数库。

以上特性首先由来自于标准库中的itertools和functools所提供的支持体现出来, 它们包含了像permutation, chain, 和partial这些函数来补充标准的已经存在于核心语言中的map, filter, reduce. 可是人们对这些库所蕴含的潜在功能的采纳应用却还未达到像其他语言中类似项目的水平。其原因有可能是因为还不够完整,而且缺乏一些常见的相关函数比如compose和groupby来做为核心操作的补充。

itertoolz和functoolz(注意后面的z)这两个项目首先做了完善这套函数的尝试. 这些库包含了在标准itertools和functools中缺失的几个函数. 而itertoolz/functoolz最终合并成单一的toolz项目在此阐述.

大部分的现代函数式语言(Haskell, Scala, Clojure, …) 包含一些在toolz中的变种函数. toolz项目总体上与Clojure标准库中的API保持一致, 当出现分歧时基本以该API为准. toolz的API同时也强烈的受到python语言自身的影响, 也常常做出偏向调整, 以更易于得到该社区的认可.

可组合性

Toolz函数互操作因为它们只消费和生产一套很小的, 通用的核心数据结构。每个toolz函数仅消费可迭代型(iterables), 字典以及函数, 并且每个toolz函数仅生产可迭代型(iterables), 字典以及函数. 这个标准化的接口使得我们可以组合几个通用函数来解决定制的问题.

标准接口使得我们能将很多工具放在一起使用, 甚至即使这些工具在设计时并未考虑过是应用在一起的. 我们把这种组合叫做”聚合应用”.

标准接口

这个概念可以用乐高积木的例子来作为最佳阐释。

使用乐高你可以将一个火箭引擎与滑雪板连接到一个皮划艇. 由于每块积木可以由一个简单的接口 – 一些简单、常规的5毫米圆凸起连接, 以上组合实现起来是超自然的。这种可以任意将积木块连接的自由让孩子们以各种方式放飞他们的想象力.

其他标准接口

Toolz基于一个标准接口来构建, 这种设计选择并非仅此一例. 有其他的标准接口存在并给应用领域带来巨大的好处.

在python领域, NumPy数组为数字和科学计算提供一个基础对象. 任何项目可以消费和生产Numpy数组的能力是各种SciPy项目获得广泛成功的前提. 今天, 我们也在Pandas项目的DataFrame中看到了类似的实作.

UNIX 工具集依赖于文件和流文本.

JSON作为web通信的标准接口的兴起. 当我们将其与之前辈XML来比较时, 该标准化方式的美妙性光芒闪耀. XML的设计目标是可扩展性和定制性,允许每个应用设计自己的接口. 结果产生了一种定制数据语言, 它成了陷人于难以理解的困境之海洋,而未能发展成通用的数据分析和处理的基础设施.

相反,JSON是极其限制性的,只允许固定的一套数据结构, list, dictionaries, numbers, strings. 好在这套结构在大部分现代语言中都是共通的, 使得JSON受到极其广泛的支持,其广泛性可能仅次于CSV.

函数纯粹性

如果某函数符合如下条件我们称之为’纯’:

  1. 它不依赖于隐藏的状态,或者说它仅依赖于它的输入
  2. 函数的求值不会产生副作用

简而言之纯函数的内部工作与程序的其他部分是隔离的.

例子

下面的两个例子将说明这个问题:

# 纯函数def min(x, y):    if x < y:            return x    else:            return y# 非纯函数(exponent,幂值)exponent = 2def powers(L):    for i in range(len(L)):            L[i] = L[i]**exponent    return L

min函数是纯的, 给它相同的输入总是产生相同的结果. 它也不影响任何外部变量.

powers函数是非纯的. 有两个原因. 首先, 它依赖于一个全局变量, exponent. 其次, 它改变了输入变量L, 而该变量可能有一个外部状态. 考虑以下执行:

>>> data = [1, 2, 3]>>> result = powers(data)>>> print result[1, 4, 9]>>> print data[1, 4, 9]

我们看到powers影响了变量data。 使用我们函数的用户可能会惊讶于这个结果. 一般我们希望我们的输入是不变的.

当我们在一个不同的上下文运行这段代码时,会出现另一个问题:

>>> data = [1, 2, 3]>>> result = powers(data)>>> print result[1, 8, 27]

当我们给power同样的输入却得到不同的输出. 怎么会这样? 某人可能已经把exponent的值改成了3。产出了立方值而不是平方值. 一开始这种灵活性看起来是一个特性, 而且在很多时候也确实是. 这种灵活性的代价是,无论何时使用powers,我们都必须额外地跟踪exponent变量. 当我们使用更多的函数时,这种额外的变量变成一种负担.

状态

非纯函数常常更加有效率但是也需要程序员”跟踪”几个变量的状态. 随着程序的大小增加, 跟踪这种状态会变得更加困难. 避开状态使得程序员在概念上解决大得多的问题. 相比自在的信任你的函数可以由输入产生期望的结果,性能上的损失常常可以忽略.

测试

附带的好处是, 测试纯函数比测试非纯函数要简单的多。测试过有随机性表现函数的程序员会有切身体会.

惰性

懒迭代器仅在必要时计算. 它们允许我们语义上操作大量的数据同时实际保持很少的一部分在内存中. 它们像不占空间的list.

例子 - 双城记

我们打开一个文件内有狄更斯的经典小说“双城记”文本.

>>> book = open('tale-of-two-cities.txt')

很像一个小学生,python拥有和打开了该文件而没有读一行文本. 对象book是一个懒迭代器! Python仅在我们明确的请求时给我们一行文本:

>>>next(book)"It was the best of times,">>>next(book)"It was the worst of times,"

如此继续. 每次我们在book上调用next时, 我们拿到另一行文本, book迭代器缓慢地在文本中行进.

计算

我们可以在懒迭代器上实行懒操作而无需做任何实际计算. 比如我们以大写方式读入此书

>>> from toolz import map  # toolz' map 默认是惰性的>>> loud_book = map(str.upper, book)>>> next(loud_book)"IT WAS THE AGE OF WISDOM,">>> next(loud_book)"IT WAS THE AGE OF FOOLISHNESS," 

看起来像是我们将函数str.upper应用在书内的每一行, 操作即刻就完成了。 实际上Python只在需要的时候做大写操作, 比如当你调用next获取另一行文本时。

Reductions(聚合)

你可以像操作list, tuples, sets一样操作懒迭代器,你可以在for循环中使用它们

for line in loud_book:    ...

你可以将它们全部实例化到内存, 用list或者tuple的构造器调用它们

loud_book = list(loud_book)

当然如果它们非常大这种做法就可能不太明智了. 我们常使用惰性来避免一次性加载大量的数据集合到内存。很多在大数据集上的计算不需要一次获取所有的数据. 特别是聚合(reductions)(类似求和) 常常接受大量的序列型数据([1,2,3,4])然后输出操纵性(managable)好得多的结果(10), 只要每次存取一点点数据就行。比方说我们可以对双城记所有字母进行计数, 简单的使用来自于toolz的函数:

>>> from toolz import concat, frequencies>>> letters = frequencies(concat(loud_book)){ 'A': 48036,'B': 8402,'C': 13812,'D': 28000,'E': 74624,...

在这个例子中frequencies(频度)是某种类型的聚合. 任意时刻我们都只是需要双城记的区区几百字节在内存中。 我们甚至可以轻而易举地把这个计算应用到整个古登堡计划的藏书或维基百科。

控制流

当我们总是需要同时考虑几个概念时, 编程是一项辛苦的工作。好的编程是把大的问题切分成小的问题,并将小的解决方案构建成大的解决方案. 以这种实践方式,同时思考的需求被限制在一次几个元素上。

所有的现代编程语言都提供基于数据结构构造数据以及从其他函数构造函数的机制。除了数据和函数,编程的第三个元素是控制流. 在简单控制流上构建复杂的控制流式更具挑战性.

此言怎讲?

在一个计算机程序上每个元素都属于下例中的一类:

  • 一个变量或文字值如 x, total, 5
  • 一个函数或求值如 x+1中的+, fib(3)的函数fib, 在line.split(‘,’)中的方法split, 或 x=0中的=
  • 控制流如if, for, return

下例是一段代码,看看你能否给每个条目贴上变量/值, 函数/求值, 或控制流的标签

def fib(n):    a, b = 0, 1    for i in range(n):            a, b = b, a + b    return b

当我们需要同时判断很多类型的代码元素时, 编程是件辛苦的差事。好的编程是管理好这三个元素,让程序员只需要在同一时间考虑它们的一小部分。比如我们可能将很多整数变量放入一个整型列表, 或者从小的函数构建出大的函数。管理数据和函数能以自然的方式进行,而控制流的处理带给我们更多的挑战.

我们把数据组织成数据结构, 像list, dictionary, 或对象, 以分组相关的数据. 这使得我们可以像操纵单个实体一样处理大量集合的相关数据。

我们从小的函数上构建起大的函数, 使得我们可以把像洗衣这种复杂的任务分割成序列化的小任务

def do_laundry(clothes):    wet_clothes = wash(clothes, coins)    dry_clothes = dry(wet_clothes, coins)    return fold(dry_clothes)

控制流更具挑战性,我们怎样把复杂的控制流分割成简单的小块以适合装入我们的大脑?我们怎样封装常见的递归模式?

让我们用一个常用的控制结构作为例子来启动吧, 这就是将一个函数应用到list的每个元素。 想象我们想下载几个web页面的HTML源文本,

from urllib import urlopenurls = ['http://www.google.com', 'http://www.wikipedia.com', 'http://www.apple.com']html_texts = []for item in urls:    html_texts.append(urlopen(item))return html_texts

或者我们想对一套整数计算斐波那契数字

integers = [1,2,3,4,5]fib_integers = []for item in integers:    fib_integers.append(fib(item))return fib_integers

这两个不相关的应用拥有同样的控制流模式: 它们将一个函数(urlopen/fib)应用到输入列表中的每个元素上(urls/integers), 添加结果到输出列表里. 因为这种控制流模式太常见了,我们给它起名为map, 然后我们说我们map一个函数(urlopen)到一个列表上(urls)

因为python可以把函数当成变量对待,所以我们可以把这个控制模式写成一个high-order函数,

def map(function, sequence):    output = []    for item in sequence:            output.append(function(item))    return output

这使得我们可以简化我们的代码,

html_texts = map(urlopen, urls)fib_integers = map(fib, integers)

有经验的Python程序员会知道这个控制模式太流行了,而且都发展成一种句法,流行的列表推导:

html_texts = [urlopen(url) for url in urls]

干嘛还说?

那么你可能都知道map, 而且也不用它,或者你只是更乐意用列表推导。你干嘛还要读下去?

管理复杂度

Higher order 函数map给我们一个名字来调用一个特别的控制结构. 不管你是用一个for循环, 一个列表推导, 还是map, 能够识别一个操作并给它一个名字就是有用的。为控制模式起名让我们在处理复杂问题时不用以死记硬背的方式给大脑增加负担。它与捆绑数据到数据结构或者在简单函数上构造复杂函数一样重要.

为控制流模式命名使程序员可以处理日益复杂的操作

其他模式

函数map有不少朋友. 高级程序员可能知道map的兄弟, filter和reduce. filter控制模式也可以由列表推导处理, reduce也常常被直接的for循环代替, 如果你不想使用它们,也不会有实践上的在意它们的理由。

大部分的程序员却不知道map/filter/reduce是有很多兄弟的. 比如下面的无名英雄, groupby, 一个简单的例子, 按名字的长度来分组:

>>> names = ['Alice', 'Bob', 'Charlie', 'Dan', 'Edith', 'Frank']>>> groupby(len, names){3: ['Bob', 'Dan'], 5: ['Alice', 'Edith', 'Frank'], 7: ['Charlie']}

再看一个例子, 按奇偶分组数字

>>> def iseven(n):...     return n % 2 == 0>>> groupby(iseven, [1, 2, 3, 4, 5, 6, 7]){True: [2, 4, 6], False: [1, 3, 5, 7]}

如果我们要手写第二个例子可能会是这样:

evens = []odds = []for item in numbers:    if iseven(item):        evens.append(item)    else:        odds.append(item)

大部分的程序猿编写完全像这样的代码一遍又一遍了,就像他们可能已经重复过map控制模式。当我们从代码中认出groupby操作我们就从思想上把操作的细节”折叠“成单一的概念。

Toolz库包含了一打像map和groupby的模型。学习一套核心集合涵盖了绝大部分通常用手工完成的常见编程任务。

一套丰富的核心控制函数语汇带来如下的好处:

  • 你识别出新模式
  • 你可以比死记硬背编码少犯错误
  • 你依赖于经过良好测试和基准测试的实现

但这些好处并不免费获得。正如口语中使用丰富语汇将疏远新入的实践者,大部分的函数式编程语言已经落入这个陷阱且被视为不易近人甚而有不明觉历之虞。而Python保持着俗汉的口碑且得益于斯。恰如口语中操弄词藻之度亦当考量受众之理解力而有所节制乎。

柯里化(Curry)

传统上部分求值函数主要由partial函数来处理。柯里化提供了句法糖:

>>> double = partial(mul, 2)    # 部分求值, mul(x,y) ==> double(6)=12, double(8)=16>>> double = mul(2)             # 柯里化 (即double=partial(mul,2), 上一句的简写)

当程序员链接几个higher order函数在一起的时候, 该句法糖比较有用(少敲打键盘)

部分求值

通常在把小的函数组合成大函数时,我们需要部分求值

>>> def stem(word):...     """ 把单词以原始样式摘取出来(去掉前面和后面的各种可能的符号) """...     return word.lower().rstrip(",.!:;'-\"").lstrip("'\"")>>> wordcount = compose(frequencies, partial(map, stem), str.split) # map函数标准格式 map(f 函数,  data 数据), 通过partial可以只指定函数参数,如上面的stem,而数据参数未定, # 在函数组合里 compose会把下一个函数str.split的结果送给partial的未指定参数,  即成为map中的数据参数.

在此我们想map stem函数到每一个由str.split产生的列表元素中.

柯里化

在此上下文中, 柯里化仅是一个部分求值的句法糖. 一个柯里化函数如果未收到足够的参数来计算结果时将自动变成部分求值

>>> from toolz import curry>>> @curry              # We can use curry as a decorator... def mul(x, y):...     return x * y>>> double = mul(2)     # mul 没有收到足够的参数来求值...                     # 所以它持有2并等待, 返回一个部分求值的函数double...                     >>> double(5)10

因此如果map被柯里化…

>>>map=curry(map)

我们就可以简写partial形式, 如下

>>> # wordcount = compose(frequencies, partial(map, stem), str.split)>>> wordcount = compose(frequencies, map(stem), str.split)

在这个特别的例子里你或许就简单的保持使用partial, 一旦partial在你的代码里出现几次以后再切换到curry

Curried名字空间

所有出现在toolz名字空间的函数都在toolz.curried名字空间里被柯里化

所以你可以用如下方式做import

>>> from toolz.curried import *

如此一来所有你喜爱的toolz函数都自动的被柯里化了.

……..

翻译到这里我以为该文档的精华就到此了, PyToolz对标准库是个强大有力的函数库补充, 后面的官方文档还有很多高级特性, 但基础思想基本都写在上面了。有兴趣的同学可以移步此处继续深入了解: http://toolz.readthedocs.org/en/latest/

0 0