Python函数式编程指南(二):从函数开始


Posted in Python onJune 24, 2015

2. 从函数开始
2.1. 定义一个函数
如下定义了一个求和函数:

def add(x, y):

    return x + y

关于参数和返回值的语法细节可以参考其他文档,这里就略过了。

使用lambda可以定义简单的单行匿名函数。lambda的语法是:

lambda args: expression

参数(args)的语法与普通函数一样,同时表达式(expression)的值就是匿名函数调用的返回值;而lambda表达式返回这个匿名函数。如果我们给匿名函数取个名字,就像这样:
lambda_add = lambda x, y: x + y

这与使用def定义的求和函数完全一样,可以使用lambda_add作为函数名进行调用。然而,提供lambda的目的是为了编写偶尔为之的、简单的、可预见不会被修改的匿名函数。这种风格虽然看起来很酷,但并不是一个好主意,特别是当某一天需要对它进行扩充,再也无法用一个表达式写完时。如果一开始就需要给函数命名,应该始终使用def关键字。

2.2. 使用函数赋值

事实上你已经见过了,上一节中我们将lambda表达式赋值给了add。同样,使用def定义的函数也可以赋值,相当于为函数取了一个别名,并且可以使用这个别名调用函数:

add_a_number_to_another_one_by_using_plus_operator = add

print add_a_number_to_another_one_by_using_plus_operator(1, 2)

既然函数可以被变量引用,那么将函数作为参数和返回值就是很寻常的做法了。

2.3. 闭包
闭包是一类特殊的函数。如果一个函数定义在另一个函数的作用域中,并且函数中引用了外部函数的局部变量,那么这个函数就是一个闭包。下面的代码定义了一个闭包:

def f():

    n = 1

    def inner():

        print n

    inner()

    n = 'x'

    inner()

函数inner定义在f的作用域中,并且在inner中使用了f中的局部变量n,这就构成了一个闭包。闭包绑定了外部的变量,所以调用函数f的结果是打印1和'x'。这类似于普通的模块函数和模块中定义的全局变量的关系:修改外部变量能影响内部作用域中的值,而在内部作用域中定义同名变量则将遮蔽(隐藏)外部变量。

如果需要在函数中修改全局变量,可以使用关键字global修饰变量名。Python 2.x中没有关键字为在闭包中修改外部变量提供支持,在3.x中,关键字nonlocal可以做到这一点:

#Python 3.x supports `nonlocal'

def f():

    n = 1

    def inner():

        nonlocal n

        n = 'x'

    print(n)

    inner()

    print(n)

调用这个函数的结果是打印1和'x',如果你有一个Python 3.x的解释器,可以试着运行一下。

由于使用了函数体外定义的变量,看起来闭包似乎违反了函数式风格的规则即不依赖外部状态。但是由于闭包绑定的是外部函数的局部变量,而一旦离开外部函数作用域,这些局部变量将无法再从外部访问;另外闭包还有一个重要的特性,每次执行至闭包定义处时都会构造一个新的闭包,这个特性使得旧的闭包绑定的变量不会随第二次调用外部函数而更改。所以闭包实际上不会被外部状态影响,完全符合函数式风格的要求。(这里有一个特例,Python 3.x中,如果同一个作用域中定义了两个闭包,由于可以修改外部变量,他们可以相互影响。)

虽然闭包只有在作为参数和返回值时才能发挥它的真正威力,但闭包的支持仍然大大提升了生产率。

2.4. 作为参数
如果你对OOP的模板方法模式很熟悉,相信你能很快速地学会将函数当作参数传递。两者大体是一致的,只是在这里,我们传递的是函数本身而不再是实现了某个接口的对象。
我们先来给前面定义的求和函数add热热身:

print add('三角形的树', '北极')

与加法运算符不同,你一定很惊讶于答案是'三角函数'。这是一个内置的彩蛋...bazinga!

言归正传。我们的客户有一个从0到4的列表:

lst = range(5) #[0, 1, 2, 3, 4]

虽然我们在上一小节里给了他一个加法器,但现在他仍然在为如何计算这个列表所有元素的和而苦恼。当然,对我们而言这个任务轻松极了:
amount = 0

for num in lst:

    amount = add(amount, num)

这是一段典型的指令式风格的代码,一点问题都没有,肯定可以得到正确的结果。现在,让我们试着用函数式的风格重构一下。

首先可以预见的是求和这个动作是非常常见的,如果我们把这个动作抽象成一个单独的函数,以后需要对另一个列表求和时,就不必再写一遍这个套路了:

def sum_(lst):

    amount = 0

    for num in lst:

        amount = add(amount, num)

    return amount

 

print sum_(lst)

还能继续。sum_函数定义了这样一种流程:
1. 使用初始值与列表的第一个元素相加;
2. 使用上一次相加的结果与列表的下一个元素相加;
3. 重复第二步,直到列表中没有更多元素;
4. 将最后一次相加的结果返回。

如果现在需要求乘积,我们可以写出类似的流程——只需要把相加换成相乘就可以了:

def multiply(lst):

    product = 1

    for num in lst:

        product = product * num

    return product

除了初始值换成了1以及函数add换成了乘法运算符,其他的代码全部都是冗余的。我们为什么不把这个流程抽象出来,而将加法、乘法或者其他的函数作为参数传入呢?

def reduce_(function, lst, initial):

    result = initial

    for num in lst:

        result = function(result, num)

    return result

 

print reduce_(add, lst, 0)

现在,想要算出乘积,可以这样做:
print reduce_(lambda x, y: x * y, lst, 1)

那么,如果想要利用reduce_找出列表中的最大值,应该怎么做呢?请自行思考:)

虽然有模板方法这样的设计模式,但那样的复杂度往往使人们更情愿到处编写循环。将函数作为参数完全避开了模板方法的复杂度。

Python有一个内建函数reduce,完整实现并扩展了reduce_的功能。本文稍后的部分包含了有用的内建函数的介绍。请注意我们的目的是没有循环,使用函数替代循环是函数式风格区别于指令式风格的最显而易见的特征。

*像Python这样构建于类C语言之上的函数式语言,由于语言本身提供了编写循环代码的能力,内置函数虽然提供函数式编程的接口,但一般在内部还是使用循环实现的。同样的,如果发现内建函数无法满足你的循环需求,不妨也封装它,并提供一个接口。

2.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:

lst = map_(lambda x: add(1, x), lst)

print reduce_(lambda x, y: x * y, lst, 1)

答案是120,这还远远不够大。再来:
lst = map_(lambda x: add(10, x), lst)

print reduce_(lambda x, y: x * y, lst, 1)

?澹?率瞪衔艺娴拿挥邢氲酱鸢富崾?60360,我发誓没有收周鸿?任何好处。

现在回头看看我们写的两个lambda表达式:相似度超过90%,绝对可以使用抄袭来形容。而问题不在于抄袭,在于多写了很多字符有木有?如果有一个函数,根据你指定的左操作数,能生成一个加法函数,用起来就像这样:

lst = map_(add_to(10), lst) #add_to(10)返回一个函数,这个函数接受一个参数并加上10后返回

写起来应该会舒服不少。下面是函数add_to的实现:

def add_to(n):

    return lambda x: add(n, x)

通过为已经存在的某个函数指定数个参数,生成一个新的函数,这个函数只需要传入剩余未指定的参数就能实现原函数的全部功能,这被称为偏函数。Python内置的functools模块提供了一个函数partial,可以为任意函数生成偏函数:
functools.partial(func[, *args][, **keywords])

你需要指定要生成偏函数的函数、并且指定数个参数或者命名参数,然后partial将返回这个偏函数;不过严格的说partial返回的不是函数,而是一个像函数一样可直接调用的对象,当然,这不会影响它的功能。

另外一个特殊的例子是装饰器。装饰器用于增强甚至干脆改变原函数的功能,我曾写过一篇文档介绍装饰器,地址在这里:https://3water.com/article/59867.htm。

*题外话,单就例子中的这个功能而言,在一些其他的函数式语言中(例如Scala)可以使用名为柯里化(Currying)的技术实现得更优雅。柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。如下的伪代码所示:

#不是真实的代码

def add(x)(y): #柯里化

    return x + y

 

lst = map_(add(10), lst)

通过将add函数柯里化,使得add接受第一个参数x,并返回一个接受第二个参数y的函数,调用该函数与前文中的add_to完全相同(返回x + y),且不再需要定义add_to。看上去是不是更加清爽呢?遗憾的是Python并不支持柯里化。

2.6. 部分内建函数介绍

reduce(function, iterable[, initializer])

这个函数的主要功能与我们定义的reduce_相同。需要补充两点:
它的第二个参数可以是任何可迭代的对象(实现了__iter__()方法的对象);
如果不指定第三个参数,则第一次调用function将使用iterable的前两个元素作为参数。
由reduce和一些常见的function组合成了下面列出来的内置函数:

all(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

此篇结束:)

Python 相关文章推荐
以Flask为例讲解Python的框架的使用方法
Apr 29 Python
python实现自动登录人人网并采集信息的方法
Jun 28 Python
浅谈python中的实例方法、类方法和静态方法
Feb 17 Python
Python实现一个转存纯真IP数据库的脚本分享
May 21 Python
Python中sort和sorted函数代码解析
Jan 25 Python
Django 实现下载文件功能的示例
Mar 06 Python
对numpy.append()里的axis的用法详解
Jun 28 Python
python查看列的唯一值方法
Jul 17 Python
python去重,一个由dict组成的list的去重示例
Jan 21 Python
python issubclass 和 isinstance函数
Jul 25 Python
Matplotlib使用字符串代替变量绘制散点图的方法
Feb 17 Python
Django模型层实现多表关系创建和多表操作
Jul 21 Python
Python函数式编程指南(一):函数式编程概述
Jun 24 #Python
web.py在SAE中的Session问题解决方法(使用mysql存储)
Jun 24 #Python
Python实现LRU算法的2种方法
Jun 24 #Python
Python中线程编程之threading模块的使用详解
Jun 23 #Python
Python Property属性的2种用法
Jun 21 #Python
Python中实现三目运算的方法
Jun 21 #Python
Python中有趣在__call__函数
Jun 21 #Python
You might like
PHP字符串的递增和递减示例介绍
2014/02/11 PHP
使用ob系列函数实现PHP网站页面静态化
2014/08/13 PHP
php的mssql数据库连接类实例
2014/11/28 PHP
php需登录的文件上传管理系统
2020/03/21 PHP
javascript 强制刷新页面的实现代码
2009/12/13 Javascript
十分钟打造AutoComplete自动完成效果代码
2009/12/26 Javascript
jQuery 性能优化手册 推荐
2010/02/23 Javascript
jQuery Select(单选) 模拟插件 V1.3.62 改进版
2010/07/17 Javascript
javascript的回调函数应用示例
2014/02/20 Javascript
JavaScript中的console.assert()函数介绍
2014/12/29 Javascript
jquery实现select下拉框美化特效代码分享
2015/08/18 Javascript
基于Bootstrap实现tab标签切换效果
2020/04/15 Javascript
微信小程序 使用腾讯地图SDK详解及实现步骤
2017/02/28 Javascript
ECMAScript6 新特性范例大全
2017/03/24 Javascript
微信小程序页面传值实例分析
2017/04/19 Javascript
vue transition 在子组件中失效的解决
2019/11/12 Javascript
[01:52]PWL S2开团时刻第四期——DOTA2成语故事
2020/12/03 DOTA
Python守护进程(daemon)代码实例
2015/03/06 Python
使用PyCharm配合部署Python的Django框架的配置纪实
2015/11/19 Python
在Django admin中编辑ManyToManyField的实现方法
2019/08/09 Python
python+Django实现防止SQL注入的办法
2019/10/31 Python
python传到前端的数据,双引号被转义的问题
2020/04/03 Python
序列化Python对象的方法
2020/08/01 Python
Eastbay官网:美国最大的运动鞋网络零售商
2016/07/27 全球购物
美国小蜜蜂Burt’s Bees德国官网:天然唇部、皮肤和身体护理产品
2020/06/14 全球购物
最新党员的自我评价分享
2013/11/04 职场文书
回门宴答谢词
2014/01/13 职场文书
初婚初育证明
2014/01/14 职场文书
年会活动策划方案
2014/01/23 职场文书
2014年会策划方案
2014/05/11 职场文书
党员群众路线教育实践活动剖析材料
2014/10/10 职场文书
预备党员自我批评思想汇报
2014/10/10 职场文书
2015年学校信息技术工作总结
2015/05/25 职场文书
小学大队委竞选口号
2015/12/25 职场文书
详解使用 CSS prefers-* 规范提升网站的可访问性与健壮性
2021/05/25 HTML / CSS
JavaScript实现九宫格拖拽效果
2022/06/28 Javascript