编程语言支持通过以下几种方式来解构具体问题:
一些语言的设计者选择强调一种特定的编程方式。这通常会让以不同的方式来编写程序变得困难。其他多范式语言则支持几种不同的编程方式。Lisp,C++和Python都是多范式语言;使用这些语言,你可以编写主要为过程式,面向对象或者函数式的程序和函数库。在大型程序中,不同的部分可能会采用不同的方式编写;比如GUI可能是面向对象的而处理逻辑则是过程式或者函数式。
在函数式程序里,输入会流经一系列函数。每个函数接受输入并输出结果。函数式风格反对使用带有副作用的函数,这些副作用会修改内部状态,或者引起一些无法体现在函数的返回值中的变化。完全不产生副作用的函数被称作“纯函数”。消除副作用意味着不能使用随程序运行而更新的数据结构;每个函数的输出必须只依赖于输入。
函数式风格的Python程序并不会极端到消除所有I/O或者赋值的程度;相反,他们会提供像函数式一样的接口,但会在内部使用非函数式的特性。比如,函数的实现仍然会使用局部变量,但不会修改全局变量或者有其他副作用。
函数式编程可以被认为是面向对象编程的对立面。对象就像是颗小胶囊,包裹着内部状态和随之而来的能让你修改这个内部状态的一组调用方法,以及由正确的状态变化所构成的程序。函数式编程希望尽可能地消除状态变化,只和流经函数的数据打交道。在Python里你可以把两种编程方式结合起来,在你的应用(电子邮件信息,事务处理)中编写接受和返回对象实例的函数。
函数式设计在工作中看起来是个奇怪的约束。为什么你要消除对象和副作用呢?不过函数式风格有其理论和实践上的优点:
一个理论上的优点是,构造数学证明来说明函数式程序是正确的相对更容易些。
证明程序正确性所用到的技术是写出不变量,也就是对于输入数据和程序中的变量永远为真的特性。然后对每行代码,你说明这行代码执行前的不变量X和Y以及执行后稍有不同的不变量X'和Y'为真。如此一直到程序结束,这时候在程序的输出上,不变量应该会与期望的状态一致。
函数式编程之所以要消除赋值,是因为赋值在这个技术中难以处理;赋值可能会破坏赋值前为真的不变量,却并不产生任何可以传递下去的新的不变量。
不幸的是,证明程序的正确性很大程度上是经验性质的,而且和Python软件无关。即使是微不足道的程序都需要几页长的证明;一个中等复杂的程序的正确性证明会非常庞大,而且,极少甚至没有你日常所使用的程序(Python解释器,XML解析器,浏览器)的正确性能够被证明。即使你写出或者生成一个证明,验证证明也会是一个问题;里面可能出了差错,而你错误地相信你证明了程序的正确性。
函数式编程的一个更实用的优点是,它强制你把问题分解成小的方面。因此程序会更加模块化。相对于一个进行了复杂变换的大型函数,一个小的函数更明确,更易于编写,也更易于阅读和检查错误。
测试和调试函数式程序相对来说更容易。
调试很简单是因为函数通常都很小而且清晰明确。当程序无法工作的时候,每个函数都是一个可以检查数据是否正确的接入点。你可以通过查看中间输入和输出迅速找到出错的函数。
测试更容易是因为每个函数都是单元测试的潜在目标。在执行测试前,函数并不依赖于需要重现的系统状态;相反,你只需要给出正确的输入,然后检查输出是否和期望的结果一致。
当你编写函数式风格的程序时,你会写出很多带有不同输入和输出的函数。其中一些不可避免地会局限于特定的应用,但其他的却可以广泛的用在程序中。举例来说,一个接受文件夹目录返回所有文件夹中的XML文件的函数;或是一个接受文件名,然后返回文件内容的函数,都可以应用在很多不同的场合。
久而久之你会形成一个个人工具库。通常你可以重新组织已有的函数来组成新的程序,然后为当前的工作写一些特殊的函数。
我会从Python的一个语言特性,编写函数式风格程序的重要基石开始说起:迭代器。
你可以手动试验迭代器的接口。
>>>L=[1,2,3]>>>iterator=iter(L)>>>t=tuple(iterator)>>>t(1,2,3)序列的解压操作也支持迭代器:如果你知道一个迭代器能够返回N个元素,你可以把他们解压到有N个元素的元组:
我们已经知道列表和元组支持迭代器。实际上,Python中的任何序列类型,比如字符串,都自动支持创建迭代器。
forlineinfile:#dosomethingforeachline...集合可以从可遍历的对象获取内容,也可以让你遍历集合的元素:
stripped_list=[line.strip()forlineinline_listifline!=""]通过列表推导式,你会获得一个Python列表;stripped_list就是一个包含所有结果行的列表,并不是迭代器。生成器表达式会返回一个迭代器,它在必要的时候计算结果,避免一次性生成所有的值。这意味着,如果迭代器返回一个无限数据流或者大量的数据,列表推导式就不太好用了。这种情况下生成器表达式会更受青睐。
生成器表达式两边使用圆括号("()"),而列表推导式则使用方括号("[]")。生成器表达式的形式为:
(expressionforexprinsequence1ifcondition1forexpr2insequence2ifcondition2forexpr3insequence3...ifcondition3forexprNinsequenceNifconditionN)再次说明,列表推导式只有两边的括号不一样(方括号而不是圆括号)。
这些生成用于输出的元素会成为expression的后继值。其中if语句是可选的;如果给定的话expression只会在符合条件时计算并加入到结果中。
生成器表达式总是写在圆括号里面,不过也可以算上调用函数时用的括号。如果你想即时创建一个传递给函数的迭代器,可以这么写:
obj_total=sum(obj.countforobjinlist_all_objects())其中for...in语句包含了将要遍历的序列。这些序列并不必须同样长,因为它们会从左往右开始遍历,而不是同时执行。对每个sequence1中的元素,sequence2会从头开始遍历。sequence3会对每个sequence1和sequence2的元素对开始遍历。
换句话说,列表推导式器是和下面的Python代码等价:
forexpr1insequence1:ifnot(condition1):continue#Skipthiselementforexpr2insequence2:ifnot(condition2):continue#Skipthiselement...forexprNinsequenceN:ifnot(conditionN):continue#Skipthiselement#Outputthevalueof#theexpression.这说明,如果有多个for...in语句而没有if语句,输出结果的长度就是所有序列长度的乘积。如果你的两个列表长度为3,那么输出的列表长度就是9:
毫无疑问,你已经对如何在Python和C中调用普通函数很熟悉了,这时候函数会获得一个创建局部变量的私有命名空间。当函数到达return表达式时,局部变量会被销毁然后把返回给调用者。之后调用同样的函数时会创建一个新的私有命名空间和一组全新的局部变量。但是,如果在退出一个函数时不扔掉局部变量会如何呢?如果稍后你能够从退出函数的地方重新恢复又如何呢?这就是生成器所提供的;他们可以被看成可恢复的函数。
这里有简单的生成器函数示例:
这里有一个generate_ints()生成器的示例:
>>>gen=generate_ints(3)>>>gen
你可以手动编写自己的类来达到生成器的效果,把生成器的所有局部变量作为实例的成员变量存储起来。比如,可以这么返回一个整数列表:把self.count设为0,然后通过count`()。然而,对于一个中等复杂程度的生成器,写出一个相应的类可能会相当繁杂。
#ArecursivegeneratorthatgeneratesTreeleavesinin-order.definorder(t):ift:forxininorder(t.left):yieldxyieldt.labelforxininorder(t.right):yieldx另外两个test_generators.py中的例子给出了N皇后问题(在NxN的棋盘上放置N个皇后,任何一个都不能吃掉另一个),以及马的遍历路线(在NxN的棋盘上给马找出一条不重复的走过所有格子的路线)的解。
在Python2.4及之前的版本中,生成器只产生输出。一旦调用生成器的代码创建一个迭代器,就没有办法在函数恢复执行的时候向它传递新的信息。你可以设法实现这个功能,让生成器引用一个全局变量或者一个调用者可以修改的可变对象,但是这些方法都很繁杂。
val=(yieldi)我建议你在处理yield表达式返回值的时候,总是两边写上括号,就像上面的例子一样。括号并不总是必须的,但是比起记住什么时候需要括号,写出来会更容易一点。
可以调用send(value)()
这里有一个简单的每次加1的计数器,并允许改变内部计数器的值。
defcounter(maximum):i=0whilei 这些改变的累积效应是,让生成器从单向的信息生产者变成了既是生产者,又是消费者。 生成器也可以成为协程,一种更广义的子过程形式。子过程可以从一个地方进入,然后从另一个地方退出(从函数的顶端进入,从return语句退出),而协程可以进入,退出,然后在很多不同的地方恢复(yield语句)。 我们可以看看迭代器常常用到的函数的更多细节。 f(iterA[0],iterB[0]),f(iterA[1],iterB[1]),f(iterA[2],iterB[2]),.... >>>defis_even(x):...return(x%2)==0>>>list(filter(is_even,range(10)))[0,2,4,6,8]这也可以写成列表推导式: 这个迭代器设计用于长度相同的可迭代对象。如果可迭代对象的长度不一致,返回的数据流的长度会和最短的可迭代对象相同 这个模块里的函数大致可以分为几类: itertools.permutations([1,2,3,4,5],2)=>(1,2),(1,3),(1,4),(1,5),(2,1),(2,3),(2,4),(2,5),(3,1),(3,2),(3,4),(3,5),(4,1),(4,2),(4,3),(4,5),(5,1),(5,2),(5,3),(5,4)itertools.permutations([1,2,3,4,5])=>(1,2,3,4,5),(1,2,3,5,4),(1,2,4,3,5),...(5,4,3,2,1)如果你不提供r参数的值,它会使用可迭代对象的长度,也就是说会排列所有的元素。 注意这些函数会输出所有可能的位置组合,并不要求可迭代对象的内容不重复: 对于用函数式风格编写的程序,有时你会希望通过给定部分参数,将已有的函数构变形称新的函数。考虑一个Python函数f(a,b,c);你希望创建一个和f(1,b,c)等价的新函数g(b,c);也就是说你给定了f()的一个参数的值。这就是所谓的“部分函数应用”。 这里有一个很小但很现实的例子: 这个模块里的一些函数: 全部函数列表可以参考operator模块的文档。 编写函数式风格程序时,你会经常需要很小的函数,作为谓词函数或者以某种方式来组合元素。 如果合适的Python内置的或者其他模块中的函数,你就一点也不需要定义新的函数: 我这么偏好的一个原因是,lambda能够定义的函数非常受限。函数的结果必须能够作为单独的表达式来计算,这意味着你不能使用多路if...elif...else比较,或者try...except语句。如果你尝试在lambda语句中做太多事情,你最终会把表达式过于复杂以至于难以阅读。你能快速的说出下面的代码做了什么事情吗?: importfunctoolsdefcombine(a,b):return0,a[1]+b[1]total=functools.reduce(combine,items)[1]如果我仅仅使用一个for循环会更好: FredrikLundh曾经建议以下一组规则来重构lambda的使用: 我非常喜欢这些规则,不过你完全有权利争辩这种消除lambda的风格是不是更好。 作者要感谢以下人员对本文各种草稿给予的建议,更正和协助:IanBicking,NickCoghlan,NickEfford,RaymondHettinger,JimJewett,MikeKrell,LeandroLameiro,JussiSalmela,CollinWinter,BlakeWinton。 0.1版:2006年6月30日发布。 0.11版:2006年7月1日发布。修正拼写错误。 0.2版:2006年7月10日发布。将genexp与listcomp两节合二为一。修正拼写错误。 0.21版:加入了tutor邮件列表中建议的更多参考文件。 0.30版:添加了有关functional模块的小节,由CollinWinter撰写;添加了有关operator模块的简短小节;其他少量修改。