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 相关文章推荐
使用XML库的方式,实现RPC通信的方法(推荐)
Jun 14 Python
python如何发布自已pip项目的方法步骤
Oct 09 Python
python: 判断tuple、list、dict是否为空的方法
Oct 22 Python
Python实现定期检查源目录与备份目录的差异并进行备份功能示例
Feb 27 Python
python批量修改文件夹及其子文件夹下的文件内容
Mar 15 Python
python变量命名的7条建议
Jul 04 Python
python用requests实现http请求代码实例
Oct 31 Python
Python如何执行精确的浮点数运算
Jul 31 Python
python3爬虫中多线程的优势总结
Nov 24 Python
Python logging自定义字段输出及打印颜色
Nov 30 Python
详解python第三方库的安装、PyInstaller库、random库
Mar 03 Python
python中os.path.join()函数实例用法
May 26 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_Flame(Version:Progress)的原代码
2006/10/09 PHP
详解PHP的Laravel框架中Eloquent对象关系映射使用
2016/02/26 PHP
php同时使用session和cookie来保存用户登录信息的实现代码
2016/05/13 PHP
IE和FireFox(FF)中js和css的不同
2009/04/13 Javascript
jQuery实现鼠标经过图片预览大图效果
2014/04/10 Javascript
jQuery中noConflict()用法实例分析
2015/02/08 Javascript
js实现仿百度汽车频道选择汽车图片展示实例
2015/05/06 Javascript
jqueryMobile 动态添加元素,展示刷新视图的实现方法
2016/05/28 Javascript
AngularJs 指令详解及示例代码
2016/09/01 Javascript
js实现图片360度旋转
2017/01/22 Javascript
jQuery.ajax向后台传递数组问题的解决方法
2017/05/12 jQuery
vue 实现左右拖拽元素并且不超过他的父元素的宽度
2018/11/30 Javascript
详解vue中移动端自适应方案
2019/05/05 Javascript
[01:23:35]Ti4主赛事胜者组 DK vs EG 1
2014/07/19 DOTA
python网络编程之UDP通信实例(含服务器端、客户端、UDP广播例子)
2014/04/25 Python
Python中的map、reduce和filter浅析
2014/04/26 Python
Python+Wordpress制作小说站
2017/04/14 Python
使用Python3制作TCP端口扫描器
2017/04/17 Python
python验证码识别教程之滑动验证码
2018/06/04 Python
Python 转换RGB颜色值的示例代码
2019/10/13 Python
利用python3 的pygame模块实现塔防游戏
2019/12/30 Python
Python控制台实现交互式环境执行
2020/06/09 Python
JavaScript实现页面动态验证码的实现示例
2021/03/23 Javascript
机电专业个人自荐信格式模板
2013/09/23 职场文书
大学新生军训感言
2014/02/25 职场文书
航海技术专业毕业生求职信
2014/04/06 职场文书
关于读书的演讲稿400字
2014/08/27 职场文书
敬老月活动总结
2014/08/28 职场文书
小学生2014国庆节演讲稿:祖国在我心中
2014/09/21 职场文书
趵突泉导游词
2015/02/03 职场文书
红色经典观后感
2015/06/18 职场文书
中学语文教学反思
2016/02/16 职场文书
小学美术教学反思
2016/02/17 职场文书
五年级作文之学校的四季
2019/12/05 职场文书
MySQL8.0升级的踩坑历险记
2021/11/01 MySQL
JavaScript原型链详解
2021/11/07 Javascript