11.7 函数式编程
Python不是也不大可能会成为一种函数式编程语言,但是它支持许多有价值的函数式编程语言构建。也有些表现得像函数式编程机制但是从传统上也不能被认为是函数式编程语言的构建。Python提供的以四种内建函数和lambda表达式的形式出现。
11.7.1 匿名函数与lambda
python允许用lambda关键字创造匿名函数。匿名是因为不需要以标准的方式来声明,比如说,使用def语句(除非赋值给一个局部变量,这样的对象也不会在任何的名称空间内创建名字)。然而,作为函数,它们也能有参数。一个完整的lambda“语句”代表了一个表达式,这个表达式的定义体必须和声明放在同一行。我们现在来演示下匿名函数的语法:
参数是可选的,如果使用的参数话,参数通常也是表达式的一部分。
核心笔记:lambda表达式返回可调用的函数对象。
用合适的表达式调用一个lambda生成一个可以像其他函数一样使用的函数对象。它们可被传给其他函数,用额外的引用别名化,作为容器对象以及作为可调用的对象被调用(如果需要的话,可以带参数)。当被调用的时候,如果给定相同的参数的话,这些对象会生成一个和相同表达式等价的结果。它们和那些返回等价表达式计算值相同的函数是不能区分的。
在我们看任何一个使用lambda的例子之前,我们意欲复习下单行语句,然后展示下lambda表达式的相似之处。
上面的函数没有带任何的参数并且总是返回True。Python中单行函数可以和标题写在同一行。如果那样的话,我们重写下我们的true()函数以使其看其来像如下的东西:
在整这个章节,我们将以这样的方式呈现命名函数,因为这有助于形象化与它们等价的lamdba表达式。至于我们的true()函数,使用lambda的等价表达式(没有参数,返回一个True)为:
命名的true()函数的用法相当的明显,但lambda就不是这样。我们仅仅是这样用,或者我们需要在某些地方用它进行赋值吗?一个lambda函数自己就是无目地服务,正如在这里看到的:
在上面的例子中,我们简单地用lambda创建了一个函数(对象),但是既没有在任何地方保存它,也没有调用它。这个函数对象的引用计数在函数创建时被设置为True,但是因为没有引用保存下来,计数又回到零,然后被垃圾回收掉。为了保留住这个对象,我们将它保存到一个变量中,以后可以随时调用。现在可能就是一个好机会。
这里用它赋值看起来非常有用。相似地,我们可以把lambda表达式赋值给一个如列表和元组的数据结构,其中,基于一些输入标准,我们可以选择哪些函数可以执行,以及参数应该是什么(在下一部分中,我们将展示如何去使用带函数式编程构建的lambda表达式)。
我们现在来设计一个带两个数字或者字符串参数,返回数字之和或者已拼接的字符串的函数。我们先将展示一个标准的函数,然后再是其未命名的等价物。
默认以及可变的参数也是允许的,如下例所示:
看上去是一回事,所以我们现在将通过演示如何能在解释器中尝试这种做法,来努力让你相信:
关于lambda最后补充一点:虽然看起来lambdda是一个函数的单行版本,但是它不等同于C++的内联语句,这种语句的目的是由于性能的原因,在调用时绕过函数的栈分配。lambda表达式运作起来就像一个函数,当被调用时,创建一个框架对象。
11.7.2 内建函数apply()、filter()、map()、reduce()
在这个部分中,我们将看看apply()、filter(), map()及reduce()内建函数并给出一些如何使用它们的例子。这些函数提供了在python中可以找到的函数式编程的特征。正如你想像的一样,lambda函数可以很好地和使用了这些函数的应用程序结合起来,因为它们都带了一个可执行的函数对象,lambda表达式提供了迅速创造这些函数的机制。
- *apply()
正如前面提到的,函数调用地语法,现在允许变量参数的元组以及关键字可变参数的字典,在Pythonl.6中有效地摈弃了apply()。这个函数将来会逐步淘汰,在未来版本中最终会消失。我们在这里提及这个函数既是为了介绍下历史,也是出于维护具有applay()函数的代码的目的。
- filter()
在本章中我们研究的第二个内建函数是filter()。想像下,去一个果园,走的时候带着一包你从树上采下的苹果。如果你能通过一个过滤器,将包裹中好的苹果留下,不是一件很令人开心的事吗?这就是filter()函数的主要前提。给定一个对象的序列和一个“过滤”函数,每个序列元素都通过这个过滤器进行筛选,保留函数返回为真的对象。filter函数为已知的序列的每个元素调用给定布尔函数。每个filter返回的非零(true)值元素添加到一个列表中。返回的对象是一个从原始队列中“过滤后”的队列。
如果我们想要用纯Python编写filter(),它或许就像这样:
一种更好地理解filter()的方法就是形象化其行为。图11-1试着那样做。
图 11-1 内建函数filter()是如何工作的
在图11-1中,我们观察到我们原始队列在顶端,一个大小为n的队列,元素从eq[0], seq[1],…seq[N-1]。每一次对bool_func()的调用,举例来说,bool_func(seq[1])、bool_func(seq[0])等,每个为True或False的返回值都会回现。(因为Boolean函数的每个定义——确保你的函数确实返回一个真或假)。如果bool_func()给每个序列的元返回一个真,那个元素将会被插入到返回的序列中。当迭代整个序列已经完成,filter()返回一个新创建的序列。我们下面展示在一个使用了filer()来获得任意奇数的简短列表的脚本。该脚本产生一个较大的随机数集合,然后过滤出所有的的偶数,留给我们一个需要的数据集。当一开始编写这个例子的时候,oddnogen.py如下所示:
代码包括两个函数:odd(),确定一个整型是奇数(真)或者偶数(假)Boolean函数,以及main(),主要的驱动部件。main()的目的是来产生10个在1~100的随机数:然后调用filter()来移除掉所有的偶数。最后,先显示出我们过滤列表的大小,然后是奇数的集合。
导入和运行这个模块几次后,我们能得到如下输出:
第一次重构
在第二次浏览时,我们注意到odd()是非常的简单的以致能用一个lambda表达式替换:
第二次重构
我们已经提到list综合使用如何能成为filter()合适的替代者,如下便是:
第三次重构
我们通过整合另外的列表解析将我们最后的列表放在一起,来进一步简化我们的代码。正如你如下看到的一样,由于列表解析灵活的语法,就不再需要一个暂时的变量了(为了简单,我们用一个较短的名字将randint()倒入到我们的代码中)。
- map()
map()内建函数与filter()相似,因为它也能通过函数来处理序列。然而,不像filter()、map()将函数调用“映射”到每个序列的元素上,并返回一个含有所有返回值的列表。
在最简单的形式中,map()带一个函数和队列,将函数作用在序列的每个元素上,然后创建由每次函数应用组成的返回值列表。所以如果你的映射函数是给每个进入的数字加2,并且你将这个函数和一个数字的列表传给map(),返回的结果列表是和原始集合相同的数字集合,但是每个数字都加了2。如果我们要用Python编写这个简单形式的map()如何运作的,它可能像在图11-2中阐释的如下代码:
图 11-2 内建函数map()是如何工作的
我们可以列举一些简短的lambda函数来展示如何用map()处理实际数据:
我们已经讨论了有时map()如何被列表解析取代,所以这里我们再分析下上面的两个例子。形式更一般的map()能以多个序列作为其输入。如果是这种情况,那么map()会并行地迭代每个序列。在第一次调用时,map()会将每个序列的第一个元素捆绑到一个元组中,将func函数作用到map()上,当map()已经完成执行的时候,并将元组的结果返回到mapped_seq映射的,最终以整体返回的序列上。图11-2阐述了一个map()如何和单一的序列一起运行。如果我们用带有每个序列有N个对象的M个序列来的map(),我们前面的图表会转变成如图11-3中展示的图表那样。
图 11-3 内建函数map()如何和>1的序列一起运作
这里有些使用带多个序列的map()的例子。
上面最后的例子使用了map()和一个为None的函数对象来将不相关的序列归并在一起。这种思想在一个新的内建函数,zip,被加进来之前的python2.0是很普遍的。而zip是这样做的:
- reduce()
函数式编程的最后的一部分是reduce(),reduce使用了一个二元函数(一个接收带两个值作为输入,进行了一些计算然后返回一个值作为输出),一个序列,和一个可选的初始化器,卓有成效地将那个列表的内容“减少”为一个单一的值,如同它的名字一样。在其他的语言中,这种概念也被称作为折叠。
它通过取出序列的头两个元素,将他们传入二元函数来获得一个单一的值来实现。然后又用这个值和序列的下一个元素来获得又一个值,然后继续直到整个序列的内容都遍历完毕以及最后的值会被计算出来为止。
你可以尝试去形象化reduce如下面的等同的例子:
有些人认为reduce()合适的函数式使用每次只需要仅需要一个元素。在上面一开始的迭代中,我们拿了两个元素因为我们没有从先前的值(因为我们没有任何先前的值)中获得的一个“结果”。这就是可选初始化器出现的地方(参见下面的init变量)。如果给定初始化器,那么一开始的迭代会用初始化器和一个序列的元素来进行,接着和正常的一样进行。
如果我们想要试着用纯Python实现reduce(),它可能会是这样:
从概念上说这可能4个中最难的一个,所以我们应该再次向你演示一个例子以及一个函数式图表(见图11-4)。reduce()的“hello world”是其一个简单加法函数的应用或在这章前面看到的与之等价的lamda:
图 11-4 reduce()内建函数是如何工作的
给定一个列表,我们可以简单地创建一个循环,迭代地遍历这个列表,再将现在元素加到前面元素的累加和上,最后当循环结束就能获得所有值的总和。
使用lambda和reduce(),可以以一行代码做出相同的事情。
给出了上面的输入,reduce()函数运行了如下的算术操作。
用list的头两个元素(0,1),调用mySum()来得到1,然后用现在的结果和下一个元素2来再次调用mySum(),再从这次调用中获得结果,与下面的元素3配对然后调用mySum(),最终拿整个前面的求和,4来调用mySum()得到10, 10即为最终的返回值。
11.7.3 偏函数应用
currying的概念将函数式编程的概念和默认参数以及可变参数结合在一起。一个带n个参数,curried的函数固化第一个参数为固定参数,并返回另一个带n-1个参数函数对象,分别类似于LISP的原始函数car和cdr的行为。Currying能泛化成为偏函数应用(partial function application,PFA),这种函数将任意数量(顺序)的参数的函数转化成另一个带剩余参数的函数对象。
在某种程度上,这似乎和不提供参数,就会使用默认参数情形相似。在PFA的例子中,参数不需要调用函数的默认值,只需明确的调用集合。你可以有很多的偏函数调用,每个都能用不同的参数传给函数,这便是不能使用默认参数的原因。
这个特征是在Python2.5的时候被引入的,通过functools模块能很好地被用户调用。
1.简单的函数式例子
如何创建一个简单小巧的例子呢?我们来使用下两个简单的函数add()和mul(),两者都来自operator模块。这两个函数仅仅是我们熟悉的+和*操作符的函数式接口,举例来说,add (x, y)与x+y一样。在我们的程序中,我们经常想要给和数字加一或者乘以100。
除了大量的,如add(1,foo), add(1,bar), mul (100,foo), mul (100,bar)般的调用,拥有已存在的并使函数调用简化的函数不是一件很美妙的事吗?举例来说,addl(foo)、addl(bar)、mul100,但是却不用去实现函数addl()和mul100()?哦,现在用PFA你就可以这样做。你可以通过使用functional模块中的partial()函数来创建PFA:
这个例子或许不能让你看到PFA的威力,但是我们不得不从从某个地方开始。当调用带许多参数的函数的时候,PFA是最好的方法。使用带关键字参数的PFA也是较简单的,因为能显示给出特定的参数,要么作为curried参数,要么作为那些更多在运行时刻传入的变量,并且我们不需担心顺序。下面的一个例子来自Python文档中关于在应用程序中使用,在这些程序中需要经常将二进制(作为字符串)转换成为整型。
这个例子使用了int()内建函数并将base固定为2来指定二进制字符串转化。现在我们没有多次用相同的第二参数(2)来调用int(),比如(‘10010’,2),相反,可以只用带一个参数的新baseTwo()函数。接着给新的(部分)函数加入了新的文档并又一次很好地使用了“函数属性”(见上面的11.3.4部分),这是很好的风格。要注意的是这里需要关键字参数base。
2.警惕关键字
如果你创建了不带base关键字的偏函数,比如,baseTwo-BAD=partial (int, 2),这可能会让参数以错误的顺序传入int(),因为固定参数的总是放在运行时刻参数的左边,比如baseTwoBAD (χ)=int(2,x)。如果你调用它,它会将2作为需要转化的数字,base作为‘10010’来传入,接着产生一个异常:
由于关键字放置在恰当的位置,顺序就得固定下来,因为,如你所知,关键字参数总是出现在形参之后,所以baseTwo (x) ==int(x,base=2)。
3.简单GUI类的例子
PFA也扩展到所有可调用的东西,如类和方法。一个使用PFA的优秀的例子是提供了“部分gui模范化”。GUI小部件通常有很多的参数,如文本、长度、最大尺寸、背景和前景色、活动或者非活动,等等。如果想要固定其中的一些参数,如让所有的文本标签为蓝底白字,你可以准确地以PFA的方式,自定义为相似对象的伪模板。
这是较有用的偏函数应用的例子,或者更准确的说,“部分类实例化”……为什么呢?
在7~8行,我们给Tkinter.Button创建了“部分类实例化器”(因为那便是它的名字,而不是偏函数),固定好父类的窗口参数然后是前景色和背景色。我们创建了两个按钮bl和b2来与模板匹配,只让文本标签唯一。quit按钮(11〜12行)是稍微自定义过的,带有不同的背景色(红色,覆盖了默认的蓝色)并配置了一个回调的函数,当按钮被按下的时候,关闭窗口(另外的两个按钮没有函数,当他们被按下的时候)。
没有MyButton“模板”的话,你每次会不得不使用“完全”的语法(因为你仍然没有给全参数,由于有大量你不传入的,含有默认值的参数):
这就一个简单的GUI的截图:
当你的代码可以变得更紧凑和易读的时候,为什么要还有重复的做令人心烦的事?你能在第18章找到更多关于GUI编程的资料,在那我们着重描写了一个使用PFA的例子。从你迄今为止看到的内容中,可以发现,在以更函数化编程环境提供默认值方面,PFA带有模板以及“style-sheeting”的感觉。你可以在《Python Library Reference》(Python库参考),《What’s New in Python 2.5》文档和指定的PEP309里,关于functools模块的文档中阅读到更多关于pfa的资料。