深入Python函数编程的一些特性
绑定
细心的读者可能记得我在第1部分的函数技术中指出的限制。特别在Python中不能避免表示函数表达式的名称的重新绑定。在FP中,名称通常被理解为较长表达式的缩写,但这一说法暗示着“同一表达式总是求出相同的值”。如果标记的名称重新被绑定,这一暗示便不成立。例如,让我们定义一些在函数编程中要用到的快捷表达式,比如:
清单1.以下PythonFP部分的重新绑定要造成故障
>>>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))在程序中的两处求得两个不同的值,即使该表达式自身并没有在其参数中使用任何可变变量。
幸运的是,functional模块提供了称为Bindings的类(向Keller提议)来防止这样的重新绑定(至少在偶然情况下,Python不会阻止一心想要解除绑定的程序员)。然而使用Bindings需要一些额外的语法,这样意外就不太容易发生。在functional模块的示例中,Keller将Bindings实例命名为let(我假定在ML家族语言的let关键词的后面)。例如,我们会这样做:
清单2.具有安全重新绑定的PythonFP部分
>>> from functional import * >>>let=Bindings() >>>let.car= lambda lst:lst[0] >>>let.car= lambda lst:lst[2] Traceback(innermostlast): File"<stdin>",line1, in ? File"d:\tools\functional.py",line976, in __setattr__ raise BindingError,"Binding'%s'cannotbemodified."%name functional.BindingError:Binding'car'cannotbemodified. >>>car(range(10)) 0
很明显,真正的程序必须做一些设置来捕获“绑定错误”,而且他们被抛出也避免了一类问题的出现。
与Bindings一起,functional提供namespace函数从Bindings实例中获取命名空间(实际上是个字典)。如果希望在Bindings中定义的(不可变)命名空间中运算一个表达式,这非常容易实现。Python的eval()函数允许在命名空间中进行运算。让我们通过一个示例来弄清楚:
清单3.使用不可变命名空间的PythonFP部分
>>>let=Bindings() #"Realworld"functionnames >>>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() #"Invertedlist"functionnames >>>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这样的无函数语言中也都包括闭包这一功能。而且,Python2.1目前正想加入词汇范围限制功能,这一功能将提供闭包的大部分功能。
什么是闭包呢?SteveMajewski最近在Python新闻组提供了对这一概念的很好描述:
对象是附带过程的数据……闭包是附带数据的过程。
闭包就象是FP的Jekyll对于OOP的Hyde(角色或者也可能对调)。闭包类似对象示例,是一种将一大批数据和功能封装在一起的一种方式。
让我们回到先前的地方了解对象和闭包解决什么问题,同时了解一下问题如果没有这两样是如何解决的。函数返回的结果往往是由其计算中使用的上下文决定的。最常见的--也可能是最明显的--指定上下文的方法是向函数传递某些参数,通知函数处理什么值。但有时候“背景”和“前景”参数有着本质的区别--在这特定时刻函数正在处理的和函数为多段潜在调用而“配置”之间的区别。
当把重点放在前景的时候,有许多处理背景的方法。其中一种是简单“咬出子弹”的方法,在每次调用的时候传递函数需要的每一个参数。这种方法通常在调用链中,只要在某些地方有可能需要值,就会传递一些值(或带有多成员的结构)。以下是一个小示例:
清单4.显示cargo变量的Python部分
>>> defa (n): ...add7=b(n) ... return add7 ... >>> defb (n): ...i=7 ...j=c(i,n) ... return j ... >>> defc (i,n): ... return i+n ... >>>a(10) #Passcargovalueforusedownstream 17
在cargo示例的b()中,n除了起到传递到c()的作用外并无其他作用。另一种方法将使用全局变量:
清单5.显示全局变量的Python部分
>>>N=10 >>> defaddN (i): ... global N ... return i+N ... >>>addN(7) #AddglobalNtoargument 17 >>>N=20 >>>addN(6) #AddglobalNtoargument 26 全局变量N在任何希望调用addN()的时候起作用,但没有必要明确地传递全局背景“上下文”。另一个更Python专用的技术是将一个变量在定义时“冻结”入一个使用默认参数的函数: 清单6.显示冻结变量的Python部分 >>>N=10 >>> defaddN (i,n=N): ... return i+n ... >>>addN(5) #Add10 15 >>>N=20 >>>addN(6) #Add10(currentNdoesn'tmatter) 16
冻结变量本质上就是闭包。某些数据被“隶属”于addN()函数。对于完整的闭包,当定义addN()的时候,所有的数据在调用的时候都将可用。然而,在这个示例(或者许多更健壮的示例)中,使用默认的参数就能简单的够用了。addN()从未使用的变量并不会对其计算造成影响。
接着让我们来看一个更接近真实问题的OOP方法。年份的时间是我想起了那些“会见”风格的收集各种数据的税收程序--不必有特定的顺序--最终使用全部数据来计算。让我们创建一个简单的版本:
清单7.Python风格的税收计算类/示例
class TaxCalc: deftaxdue (self): return (self.income-self.deduct)*self.rate taxclass=TaxCalc() taxclass.income=50000 taxclass.rate=0.30 taxclass.deduct=10000 print "PythonicOOPtaxesdue=",taxclass.taxdue()
在TaxCalc类(或其实例)中,能收集一些数据--可以以任意顺序--一旦获得了所需的所有元素,就能调用这一对象的方法来完成这一大批数据的计算。所有一切都在实例中,而且,不同示例携带不同的数据。创建多示例和区别它们的数据的可能性不可能存在于"全局变量"或"冻结变量"方法中。"cargo"方法能处理这个问题,但对于扩展的示例来说,我们看到它可能是开始传递各种值的必要条件了。既然我们已讲到这,注意传递消息的OPP风格是如何处理的也非常有趣(Smalltalk或Self与此类似,一些我使用的OOPxBase变量也是如此):
清单8.Smalltalk风格(Python)的税收计算
class TaxCalc: deftaxdue (self): return (self.income-self.deduct)*self.rate defsetIncome (self,income): self.income=income return self defsetDeduct (self,deduct): self.deduct=deduct return self defsetRate (self,rate): self.rate=rate return self print "Smalltalk-styletaxesdue=",\ TaxCalc().setIncome(50000).setRate(0.30).setDeduct(10000).taxdue()
用每个"setter"来返回self使我们能把“现有的”东西看作是每个方法应用的结果。这与FP闭包方法有许多有趣的相似点。
有了Xoltar工具包,我们就能创建具有所期望的合并数据与函数特性的完整的闭包,同时还允许多段闭包(nee对象)来包含不同的包:
清单9.Python函数风格的税收计算
from functional import * taxdue= lambda :(income-deduct)*rate incomeClosure= lambda income,taxdue:closure(taxdue) deductClosure= lambda deduct,taxdue:closure(taxdue) rateClosure= lambda rate,taxdue:closure(taxdue) taxFP=taxdue taxFP=incomeClosure(50000,taxFP) taxFP=rateClosure(0.30,taxFP) taxFP=deductClosure(10000,taxFP) print "Functionaltaxesdue=",taxFP() print "Lisp-styletaxesdue=",\ incomeClosure(50000, rateClosure(0.30, deductClosure(10000,taxdue)))()
我们定义的每一个闭包函数都携带了函数范围内定义的任何值,然后将这些值绑定到函数对象的全局范围。然而,函数的全局范围看上去不必与实际模块的全局范围相同,同时与不同闭包的“全局”范围也不相同。闭包只是简单地“携带数据”。
在示例中,我们使用了一些特殊函数在闭包范围(income、deduct、rate)内放入了特定绑定。修改设计以在范围内放入任何绑定也非常简单。我们还可以在示例中使用具有细微差别的不同函数风格,当然这只是为了好玩。第一个成功的将附加值绑定入闭包范围内;使taxFP成为可变,这些“加入到闭包”的行可以任意顺序出现。然而,如果要使用如tax_with_Income这样的不可变名称,就必须将绑定行按照一定顺序排列,然后将前面的绑定传递到下一个。无论如何,一旦必需的一切被绑定入闭包的范围内,我们就调用"seeded"函数。
第二种风格看上去更接近Lisp,(对我来说更像圆括号)。如果不考虑美观,第二种风格中发生了二件有趣的事情。第一件是名称绑定完全被避免了。第二种风格是一个单一表达式而不使用语句(请参阅第1部分,讨论为什么这样会有问题)。
其它有关“Lisp风格”闭包使用的有趣例子是其与上文提到的“Smalltalk风格”消息传递方法有多少类似。两者累积了值和调用taxdue()函数/方法(如果没有正确的数据,两者在这些原始版本中都将报错)。“Smalltalk风格”在每一步之间传递对象,而“Lisp风格”传递一个连续。但若是更深一层理解,函数和面向对象编程大部分都是这样。