可爱的 Python : Python中函数式编程,第二部分
英文原文:Charming Python: Functional programming in Python, Part 2,翻译:开源中国
摘要: 本专栏继续让David对Python中的函数式编程(FP)进行介绍。读完本文,可以享受到使用不同的编程范型(paradigm)解决问题所带来的乐趣。David在本文中对FP中的多个中级和高级概念进行了详细的讲解。
一个对象就是附有若干过程(procedure)的一段数据。。。一个闭包(closure)就是附有一段数据的一个过程(procedure)。
在我讲解函数式编程的上一篇文章,第一部分,中,我介绍了FP中的一些基本概念。 本文将更加深入的对这个内容十分丰富的概念领域进行探讨。在我们探讨的大部分内容中,Bryn Keller的”Xoltar Toolkit”为我们提供一些非常有价值的帮助作用。Keller将FP中的许多强项集中到了一个很棒且很小的模块中,他在这个模块中用纯Python代码实现了这些强项。除了functional模块外,Xoltar Toolkit还包含了一个延迟(lazy)模块,对“仅在需要时”才进行求值提供了支持。许多传统的函数式语言中也都具有延迟求值的手段,这样,使用Xoltar Toolkit中的这些组件,你就可以做到使用象Haskell这样的函数式语言能够做到的大部分事情了。
绑定(Binding)
有心的读者会记得,我在第一部分中所述的函数式技术中指出过Python的一个局限。具体讲,就是Python中没有任何手段禁止对用来指代函数式表达式的名字进行重新绑定。 在FP中,名字一般是理解为对比较长的表达式的简称,但这里面隐含了一个诺言,就是“同一个表达式总是具有同一个值”。如果对用来指代的名字重新进行绑定,就会违背这个诺言。例如, 假如我们如以下所示,定义了一些要用在函数式程序中的简记表达式:
Python中由于重新绑定而引起问题的FP编程片段
|
1
2
3
4
5
6
7
8
|
>>> car = lambda lst: lst[0]>>> cdr = lambda lst: lst[1:]>>> sum2 = lambda lst: car(lst)+car(cdr(lst))>>> sum2(range(10))1>>> car = lambda lst: lst[2]>>> sum2(range(10))5 |
非常不幸,程序中完全相同的表达式sum2(range(10))在两个不同的点求得的值却不相同, 尽管在该表达式的参数中根本没有使用任何可变的(mutable)变量。
幸运的是, functional模块提供了一个叫做Bindings(由鄙人向Keller进行的提议,proposed to Keller by yours truly)的类,可以用来避免这种重新绑定(至少可以避免意外的重新绑定,Python并不阻止任何拿定主意就是要打破规则的程序员)。尽管要用Bindings类就需要使用一些额外的语法,但这么做就能让这种事故不太容易发生。 Keller在functional模块里给出的例子中,有个Bindings的实例名字叫做let(我推测这么叫是为了仿照ML族语言中的let关键字)。例如,我们可以这么做:
Python中对重新绑定进行监视后的FP编程片段
|
1
2
3
4
5
6
7
8
9
10
11
|
>>> from functional import *>>> let = Bindings()>>> let.car = lambda lst: lst[0]>>> let.car = lambda lst: lst[2]Traceback (innermost last): File "<stdin>", line 1, in ? File "d:\tools\functional.py", line 976, in __setattr__ raise BindingError, "Binding '%s' cannot be modified." % namefunctional.BindingError: Binding 'car' cannot be modified.>>> car(range(10))0 |
显而易见,在真正的程序中应该去做一些事情,捕获这种”BindingError”异常,但发出这些异常这件事,就能够避免产生这一大类的问题。
functional模块随同Bindings一起还提供了一个叫做namespace的函数,这个函数从Bindings实例中弄出了一个命名空间 (实际就是个字典) 。如果你想计算一个表达式,而该表达式是在定义于一个Bindings中的一个(不可变)命名空间中时,这个函数就可以很方便地拿来使用。Python的eval()函数允许在命名空间中进行求值。举个例子就能说明这一切:
Python中使用不可变命名空间的FP编程片段
|
1
2
3
4
5
6
7
8
9
10
11
|
>>> let = Bindings() # "Real world" function names>>> let.r10 = range(10)>>> let.car = lambda lst: lst[0]>>> let.cdr = lambda lst: lst[1:]>>> eval('car(r10)+car(cdr(r10))', namespace(let))>>> inv = Bindings() # "Inverted list" function names>>> inv.r10 = let.r10>>> inv.car = lambda lst: lst[-1]>>> inv.cdr = lambda lst: lst[:-1]>>> eval('car(r10)+car(cdr(r10))', namespace(inv))17 |
FP中有一个特别有引人关注的概念叫做闭包。实际上,闭包充分引起了很多程序员的关注,即使通常意义上的非函数式编程语言,比如Perl和Ruby,都包含了闭包这一特性。此外,Python 2.1 目前一定会添加上词法域(lexical scoping), 这样一来就提供的闭包的绝大多数功能。
那么,闭包到底是什么?Steve Majewski最近在Python新闻组中对这个概念的特性提出了一个准确的描述:
就是说,闭包就象是FP的Jekyll,OOP(面向对象编程)的 Hyde (或者可能是将这两个角色互换)(译者注:Jekyll和Hyde是一部小说中的两个人物). 和象对象实例类似,闭包是一种把一堆数据和一些功能打包一起进行传递的手段。
先让我们后退一小步,看看对象和闭包都能解决一些什么样的问题,然后再看看在两样都不用的情况下这些问题是如何得到解决的。函数返回的值通常是由它在计算过程中使用的上下文决定的。最常见可能也是最显然的指定该上下文的方式就是给函数传递一些参数,让该函数对这些参数进行一些运算。但有时候在参数的“背景”(background)和“前景”(foreground)两者之间也有一种自然的区分,也就是说,函数在某特定时刻正在做什么和函数“被配置”为处于多种可能的调用情况之下这两者之间有不同之处。
在集中处理前景的同时,有多种方式进行背景处理。一种就是“忍辱负重”,每次调用时都将函数需要的每个参数传递给函数。这通常就相对于在函数调用链中不断的将很多值(或者是一个具有很多字段的数据结构)传上传下,就是因为在链中的某个地方可能会用到这些值。下面举个简单的例子:
用了货船变量的Python代码片段
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
>>> def a(n):... add7 = b(n)... return add7...>>> def b(n):... i = 7... j = c(i,n)... return j...>>> def c(i,n):... return i+n...>>> a(10) # Pass cargo value for use downstream17 |
在上述的货船变量例子中,函数b()中的变量n毫无意义,就只是为了传递给函数c()。另一种办法是使用全局变量:
使用全局变量的Python代码片段
|
1
2
3
4
5
6
7
8
9
10
|
>>> N = 10>>> def addN(i):... global N... return i+N...>>> addN(7) # Add global N to argument17>>> N = 20>>> addN(6) # Add global N to argument26 |
全局变量N只要你想调用ddN()就可以直接使用,就不需要显式地传递这个全局背景“上下文”了。有个稍微更加Python化的技巧,可以用来在定义函数时,通过使用缺省参数将一个变量“冻结”到该函数中:
使用冻结变量的Python代码片段
|
1
2
3
4
5
6
7
8
9
|
>>> N = 10>>> def addN(i, n=N):... return i+n...>>> addN(5) # Add 1015>>> N = 20>>> addN(6) # Add 10 (current N doesn't matter)16 |
我们冻结的变量实质上就是个闭包。我们将一些数据“附加”到了addN()函数之上。对于一个完整的闭包而言,在函数addN()定义时所出现的数据,应该在该函数被调用时也可以拿到。然而,本例中(以及更多更健壮的例子中),使用缺省参数让足够的数据可用非常简单。函数addN()不再使用的变量因而对计算结构捕获产生丝毫影响。
现在让我们再看一个用OOP的方式解决一个稍微更加现实的问题。今年到了这个时候,让我想起了颇具“面试”风格的计税程序,先收集一些数据,数据不一定有什么特别的顺序,最后使用所有这些数据进行一个计算。让我们为这种情况些个简化版本的程序:
Python风格的计税类/实例
|
1
2
3
4
5
6
7
|
class TaxCalc: def taxdue(self):return (self.income-self.deduct)*self.ratetaxclass = TaxCalc()taxclass.income = 50000taxclass.rate = 0.30taxclass.deduct = 10000print"Pythonic OOP taxes due =", taxclass.taxdue() |
在我们的TaxCalc类 (或者更准确的讲,在它的实例中),我们先收集了一些数据,数据的顺序随心所欲,然后所有需要的数据收集完成后,我们可以调用这个对象的一个方法,对这堆数据进行计算。所有的一切都呆在一个实例中,而且,不同的实例可以拥有一堆不同的数据。能够创建多个实例,而多个实例仅仅是数据不同,这通过“全局变量”和“冻结变量”这两种方法是无法办到的。”货船”方法能够做到这一点,但从那个展开的例子中我们能够看出,它可能不得不在开始时就传递多个数值。讨论到这里,注意到OOP风格的消息传递方式可能会如何来解决这一问题会非常有趣(Smalltalk或者Self与此类似,我所用过的好几种xBase的变种OOP语言也是类似的):
Smalltalk风格的(Python) 计税程序
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class TaxCalc: def taxdue(self):return (self.income-self.deduct)*self.rate def setIncome(self,income): self.income = income return self def setDeduct(self,deduct): self.deduct = deduct return self def setRate(self,rate): self.rate = rate return selfprint"Smalltalk-style taxes due =", \ TaxCalc().setIncome(50000).setRate(0.30).setDeduct(10000).taxdue() |
每个”setter”方法都返回self可以让我们将每个方法调用的结果当作“当前”对象进行处理。这和FP中的闭包方式有些相似。
通过使用Xoltar toolkit,我们可以生成完整的闭包,能够将数据和函数结合起来,获得我们所需的特性;另外还可以让多个闭包(以前成为对象)包含不同的数据:
Python的函数式风格的计税程序
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
from functional import *taxdue = lambda: (income-deduct)*rateincomeClosure = lambda income,taxdue: closure(taxdue)deductClosure = lambda deduct,taxdue: closure(taxdue)rateClosure = lambda rate,taxdue: closure(taxdue)taxFP = taxduetaxFP = incomeClosure(50000,taxFP)taxFP = rateClosure(0.30,taxFP)taxFP = deductClosure(10000,taxFP)print"Functional taxes due =",taxFP()print"Lisp-style taxes due =", \ incomeClosure(50000, rateClosure(0.30, deductClosure(10000, taxdue)))() |
我们所定义的每个闭包函数可以获取函数定义范围内的任意值,然后将这些值绑定到改函数对象的全局范围之中。然而,一个函数的全局范围并不一定就是真正的模块全局范围,也和不同的闭包的“全局”范围不相同。闭包就是“将数据带”在了身边。
在我们的例子中,我们利用了一些特殊的函数把特定的绑定限定到了一个闭包作用范围之中(income, deduct, rate)。要想修改设计,将任意的绑定限定在闭包之中,也非常简单。只是为了好玩,在本例子中我们也使用了两种稍微不同的函数式风格。第一种风格连续将多个值绑定到了闭包的作用范围;通过允许taxFP成为可变的变量,这些“添加绑定”的代码行可以任意顺序出现。然而,如果我们想要使用tax_with_Income这样的不可变名字,我们就需要以特定的顺序来安排这几行进行绑定的代码,将靠前的绑定结果传递给下一个绑定。无论在哪种情况下,在全部所需数据都绑定进闭包范围之后,我们就可以调用“种子”(seeded)方法了。
第二种风格在我看来,更象是Lisp(那些括号最象了)。除去美学问题,这第二种风格有两点值得注意。第一点就是完全避免了名字绑定,变成了一个单个的表达式,连语句都没有使用(关于为什么不使用语句很重要,请参见 P第一部分)。
第二点是闭包的“Lips”风格的用法和前文给出的“Smalltalk”风格的信息传递何其相似。实际上两者都在调用taxdue()函数/方法的过程中积累了所有值(如果以这种原始的方式拿不到正确的数据,两种方式都会出错)。“Smalltalk”风格的方法中每一步传递的是一个对象,而“Lisp”风格的方法中传递是持续进行的。 但实际上,函数式编程和面向对象式编程两者旗鼓相当。
在本文中,我们干掉了函数式编程领域中更多的内容。剩下的要比以前(本小节的题目是个小玩笑;很不幸,这里还没有解释过尾递归的概念)少多了(或者可以证明也简单多了?)。阅读functional模块中的源代码是继续探索FP中大量概念的一种非常好的方法。该模块中的注释很完备,在注释里为模块中的大多数方法/类提供了相关的例子。其中有很多简化性的元函数(meta-function)本专栏里并没有讨论到的,使用这些元函数可以大大简化对其它函数的结合(combination)和交互(interaction )的处理。对于想继续探索函数式范型的Python程序员而言,这些绝对值得好好看看。
转自:http://blog.jobbole.com/35042/
可爱的 Python : Python中函数式编程,第二部分的更多相关文章
- 可爱的 Python : Python中函数式编程,第一部分
英文原文:Charming Python: Functional programming in Python, Part 1 摘要:虽然人们总把Python当作过程化的,面向对象的语言,但是他实际上包 ...
- Python基础:函数式编程
一.概述 Python是一门多范式的编程语言,它同时支持过程式.面向对象和函数式的编程范式.因此,在Python中提供了很多符合 函数式编程 风格的特性和工具. 以下是对 Python中的函数式编程 ...
- Python进阶:函数式编程实例(附代码)
Python进阶:函数式编程实例(附代码) 上篇文章"几个小例子告诉你, 一行Python代码能干哪些事 -- 知乎专栏"中用到了一些列表解析.生成器.map.filter.lam ...
- Python Decorator 和函数式编程
看到一篇翻译不错的文章,原文链接: Python Decorator 和函数式编程
- Python进阶之函数式编程(把函数作为参数)
什么是函数式编程? 什么是函数式编程? 函数:function 函数式:functional,一种编程范式 函数式编程是一种抽象计算的编程模式 函数≠函数式,比如:计算≠计算机 在计算机当中,计算机硬 ...
- Python之面向对象函数式编程
Python之面向对象函数式编程 函数式编程的根本就是用 def 去模拟数学式的编程逻辑. 类似与 y = 2*x + 1 ,当x = 3 时,函数的结果y就得7. def test(x): retu ...
- 浅谈Python中函数式编程、面向对象编程以及古怪的PythonIC
1.函数式编程作为结构化编程的一种,正在受到越来越多的重视.那么什么事函数式编程呢? 在维基百科中给出了详细的定义,函数式编程又称泛函数编程,是一种编程规范,它将函数运算视为数学上的函数计算.简单的来 ...
- python 函数对象(函数式编程 lambda、map、filter、reduce)、闭包(closure)
1.函数对象 作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 秉承着一切皆对象的理念,我们再次回头来看函数(function).函 ...
- Python学习--08函数式编程
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数. 高阶函数 Python支持高阶函数(Higher-order function). 什么是高阶函数呢?把函数作为参 ...
随机推荐
- codeforces 713D D. Animals and Puzzle 二分+二维rmq
题目链接 给一个01矩阵, 然后每个询问给出两个坐标(x1, y1), (x2, y2). 问你这个范围内的最大全1正方形的边长是多少. 我们dp算出以i, j为右下角的正方形边长最大值. 然后用二维 ...
- C++类继承中的构造函数和析构函数 调用顺序
思想: 在C++的类继承中,构造函数不能被继承(C11中可以被继承,但仅仅是写起来方便,不是真正的继承) 建立对象时,首先调用基类的构造函数,然后在调用下一个派生类的构造函数,依次类推: 析构对象时, ...
- 正式学习React (七) react-router 源码分析
学习react已经有10来天了,对于react redux react-redux 的使用流程和原理,也已经有一定的了解,在我上一篇的实战项目里,我用到了react-route,其实对它还只是 停留在 ...
- 无法关闭的QT程序——思路开阔一下,原来这么简单!
做一个无法关闭的QT程序(想关闭时要在任务管理器里关闭),看似很难, 其实它并不难,只要让程序在关闭时启动它自身就可以了. 上代码: #include <QtGui> class Temp ...
- HDOJ-1003 Max Sum(最大连续子段 动态规划)
http://acm.hdu.edu.cn/showproblem.php?pid=1003 给出一个包含n个数字的序列{a1,a2,..,ai,..,an},-1000<=ai<=100 ...
- UML_用例图
用例图主要用来描述"用户.需求.系统功能单元"之间的关系.它展示了一个外部用户能够观察到的系统功能模型图. [用途]:帮助开发团队以一种可视化的方式理解系统的功能需求. 用例图所包 ...
- they're hiring
Gather Health ⋅ Careers Careers at Gather
- Android学习总结——Activity状态保存和恢复
Android中启动一个Activity如果点击Home键该Activity是不会被销毁的,但是当进行某些操作时某些数据就会丢失,如下: Java class: package com.king.ac ...
- Java Collection 集合类大小调整带来的性能消耗
Java Collection类的某些详细实现因为底层数据存储基于数组,随着元素数量的添加,调整大小的代价非常大.随着Collection元素增长到某个上限,调整其大小可能出现性能问题. 当Colle ...
- leetcode_question_125 Valid Palindrome
Given a string, determine if it is a palindrome, considering only alphanumeric characters and ignori ...