作为参数
如果你对OOP的模板方法模式很熟悉,相信你能很快速地学会将函数当作参数传递。两者大体是一致的,只是在这里,我们传递的是函数本身而不再是实现了某个接口的对象。
我们先来给前面定义的求和函数add热热身:
1
|
print add( '三角形的树' , '北极' ) |
与加法运算符不同,你一定很惊讶于答案是'三角函数'。这是一个内置的彩蛋...bazinga!
言归正传。我们的客户有一个从0到4的列表:
1
|
lst = range ( 5 ) #[0, 1, 2, 3, 4] |
虽然我们在上一小节里给了他一个加法器,但现在他仍然在为如何计算这个列表所有元素的和而苦恼。当然,对我们而言这个任务轻松极了:
1
2
3
|
amount = 0 for num in lst: amount = add(amount, num) |
这是一段典型的指令式风格的代码,一点问题都没有,肯定可以得到正确的结果。现在,让我们试着用函数式的风格重构一下。
首先可以预见的是求和这个动作是非常常见的,如果我们把这个动作抽象成一个单独的函数,以后需要对另一个列表求和时,就不必再写一遍这个套路了:
1
2
3
4
5
6
7
|
def sum_(lst): amount = 0 for num in lst: amount = add(amount, num) return amount print sum_(lst) |
还能继续。sum_函数定义了这样一种流程:
1. 使用初始值与列表的第一个元素相加;
2. 使用上一次相加的结果与列表的下一个元素相加;
3. 重复第二步,直到列表中没有更多元素;
4. 将最后一次相加的结果返回。
如果现在需要求乘积,我们可以写出类似的流程——只需要把相加换成相乘就可以了:
1
2
3
4
5
|
def multiply(lst): product = 1 for num in lst: product = product * num return product |
除了初始值换成了1以及函数add换成了乘法运算符,其他的代码全部都是冗余的。我们为什么不把这个流程抽象出来,而将加法、乘法或者其他的函数作为参数传入呢?
1
2
3
4
5
6
7
|
def reduce_(function, lst, initial): result = initial for num in lst: result = function(result, num) return result print reduce_(add, lst, 0 ) |
现在,想要算出乘积,可以这样做:
1
|
print reduce_( lambda x, y: x * y, lst, 1 ) |
那么,如果想要利用reduce_找出列表中的最大值,应该怎么做呢?请自行思考:)
虽然有模板方法这样的设计模式,但那样的复杂度往往使人们更情愿到处编写循环。将函数作为参数完全避开了模板方法的复杂度。
Python有一个内建函数reduce,完整实现并扩展了reduce_的功能。本文稍后的部分包含了有用的内建函数的介绍。请注意我们的目的是没有循环,使用函数替代循环是函数式风格区别于指令式风格的最显而易见的特征。
*像Python这样构建于类C语言之上的函数式语言,由于语言本身提供了编写循环代码的能力,内置函数虽然提供函数式编程的接口,但一般在内部还是使用循环实现的。同样的,如果发现内建函数无法满足你的循环需求,不妨也封装它,并提供一个接口。
作为返回值
将函数返回通常需要与闭包一起使用(即返回一个闭包)才能发挥威力。我们先看一个函数的定义:
1
2
3
4
5
|
def map_(function, lst): result = [] for item in lst: result.append(function(item)) return result |
函数map_封装了最常见的一种迭代:对列表中的每个元素调用一个函数。map_需要一个函数参数,并将每次调用的结果保存在一个列表中返回。这是指令式的做法,当你知道了列表解析(list comprehension)后,会有更好的实现。
这里我们先略过map_的蹩脚实现而只关注它的功能。对于上一节中的lst,你可能发现最后求乘积结果始终是0,因为lst中包含了0。为了让结果看起来足够大,我们来使用map_为lst中的每个元素加1:
1
2
|
lst = map_( lambda x: add( 1 , x), lst) print reduce_( lambda x, y: x * y, lst, 1 ) |
答案是120,这还远远不够大。再来:
1
2
|
lst = map_( lambda x: add( 10 , x), lst) print reduce_( lambda x, y: x * y, lst, 1 ) |
囧,事实上我真的没有想到答案会是360360,我发誓没有收周鸿祎任何好处。
现在回头看看我们写的两个lambda表达式:相似度超过90%,绝对可以使用抄袭来形容。而问题不在于抄袭,在于多写了很多字符有木有?如果有一个函数,根据你指定的左操作数,能生成一个加法函数,用起来就像这样:
1
|
lst = map_(add_to( 10 ), lst) #add_to(10)返回一个函数,这个函数接受一个参数并加上10后返回 |
写起来应该会舒服不少。下面是函数add_to的实现:
1
2
|
def add_to(n): return lambda x: add(n, x) |
通过为已经存在的某个函数指定数个参数,生成一个新的函数,这个函数只需要传入剩余未指定的参数就能实现原函数的全部功能,这被称为偏函数。Python内置的functools模块提供了一个函数partial,可以为任意函数生成偏函数:
1
|
functools.partial(func[, * args][, * * keywords]) |
你需要指定要生成偏函数的函数、并且指定数个参数或者命名参数,然后partial将返回这个偏函数;不过严格的说partial返回的不是函数,而是一个像函数一样可直接调用的对象,当然,这不会影响它的功能。
另外一个特殊的例子是装饰器。装饰器用于增强甚至干脆改变原函数的功能,我曾写过一篇文档介绍装饰器,地址在这里:http://www.cnblogs.com/huxi/archive/2011/03/01/1967600.html。
*题外话,单就例子中的这个功能而言,在一些其他的函数式语言中(例如Scala)可以使用名为柯里化(Currying)的技术实现得更优雅。柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。如下的伪代码所示:
1
2
3
4
5
|
#不是真实的代码 def add(x)(y): #柯里化 return x + y lst = map_(add( 10 ), lst) |
通过将add函数柯里化,使得add接受第一个参数x,并返回一个接受第二个参数y的函数,调用该函数与前文中的add_to完全相同(返回x + y),且不再需要定义add_to。看上去是不是更加清爽呢?遗憾的是Python并不支持柯里化。
部分内建函数介绍
- reduce(function, iterable[, initializer])
这个函数的主要功能与我们定义的reduce_相同。需要补充两点:
它的第二个参数可以是任何可迭代的对象(实现了__iter__()方法的对象);
如果不指定第三个参数,则第一次调用function将使用iterable的前两个元素作为参数。
由reduce和一些常见的function组合成了下面列出来的内置函数:12345all
(iterable)
=
=
reduce
(
lambda
x, y:
bool
(x
and
y), iterable)
any
(iterable)
=
=
reduce
(
lambda
x, y:
bool
(x
or
y), iterable)
max
(iterable[, args...][, key])
=
=
reduce
(
lambda
x, y: x
if
key(x) > key(y)
else
y, iterable_and_args)
min
(iterable[, args...][, key])
=
=
reduce
(
lambda
x, y: x
if
key(x) < key(y)
else
y, iterable_and_args)
sum
(iterable[, start])
=
=
reduce
(
lambda
x, y: x
+
y, iterable, start)
- map(function, iterable, ...)
这个函数的主要功能与我们定义的map_相同。需要补充一点:
map还可以接受多个iterable作为参数,在第n次调用function时,将使用iterable1[n], iterable2[n], ...作为参数。 - filter(function, iterable)
这个函数的功能是过滤出iterable中所有以元素自身作为参数调用function时返回True或bool(返回值)为True的元素并以列表返回,与系列第一篇中的my_filter函数相同。 - zip(iterable1, iterable2, ...)
这个函数返回一个列表,每个元素都是一个元组,包含(iterable1[n], iterable2[n], ...)。
例如:zip([1, 2], [3, 4]) --> [(1, 3), (2, 4)]
如果参数的长度不一致,将在最短的序列结束时结束;如果不提供参数,将返回空列表。
除此之外,你还可以使用本文2.5节中提到的functools.partial()为这些内置函数创建常用的偏函数。
另外,pypi上有一个名为functional的模块,除了这些内建函数外,还额外提供了更多的有意思的函数。但由于使用的场合并不多,并且需要额外安装,在本文中就不介绍了。但我仍然推荐大家下载这个模块的纯Python实现的源代码看看,开阔思维嘛。里面的函数都非常短,源文件总共只有300行不到,地址在这里:http://pypi.python.org/pypi/functional