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 相关文章推荐
分享一个常用的Python模拟登陆类
Mar 29 Python
Python中MySQLdb和torndb模块对MySQL的断连问题处理
Nov 09 Python
python处理csv中的空值方法
Jun 22 Python
基于wxPython的GUI实现输入对话框(1)
Feb 27 Python
使用PyQt4 设置TextEdit背景的方法
Jun 14 Python
Python PyCharm如何进行断点调试
Jul 05 Python
使用Python的Turtle库绘制森林的实例
Dec 18 Python
python通过文本在一个图中画多条线的实例
Feb 21 Python
python 爬虫 实现增量去重和定时爬取实例
Feb 28 Python
Python关于拓扑排序知识点讲解
Jan 04 Python
Python tkinter实现日期选择器
Feb 22 Python
Python基本的内置数据类型及使用方法
Apr 13 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
MySql 按时间段查询数据方法(实例说明)
2008/11/02 PHP
PHP开发框架kohana3 自定义路由设置示例
2014/07/14 PHP
php基于base64解码图片与加密图片还原实例
2014/11/03 PHP
PHP递归遍历多维数组实现无限分类的方法
2016/05/06 PHP
Thinkphp5框架ajax接口实现方法分析
2019/08/28 PHP
2020最新版 PhpStudy V8.1版本下载安装使用详解
2020/10/30 PHP
对YUI扩展的Gird组件 Part-1
2007/03/10 Javascript
nodejs通过phantomjs实现下载网页
2015/05/04 NodeJs
Node.js事件驱动
2015/06/18 Javascript
jQuery+html5+css3实现圆角无刷新表单带输入验证功能代码
2015/08/21 Javascript
AngularJS directive返回对象属性详解
2016/03/28 Javascript
全面解析Bootstrap中tab(选项卡)的使用方法
2016/06/06 Javascript
AngularJS  $on、$emit和$broadcast的使用
2016/09/05 Javascript
快速实现jQuery多级菜单效果
2017/02/01 Javascript
基于BootStrap multiselect.js实现的下拉框联动效果
2017/07/28 Javascript
jQuery扩展方法实现Form表单与Json互相转换的实例代码
2018/09/05 jQuery
关于node-bindings无法在Electron中使用的解决办法
2018/12/18 Javascript
JS回调函数简单易懂的入门实例分析
2019/09/29 Javascript
jQuery实现穿梭框效果
2021/01/19 jQuery
[03:03]2014DOTA2国际邀请赛 EG战队专访
2014/07/12 DOTA
简单介绍Python中利用生成器实现的并发编程
2015/05/04 Python
对Python 窗体(tkinter)文本编辑器(Text)详解
2018/10/11 Python
如何基于Python批量下载音乐
2019/11/11 Python
Python字典生成式、集合生成式、生成器用法实例分析
2020/01/07 Python
python实现ftp文件传输系统(案例分析)
2020/03/20 Python
python实现一个猜拳游戏
2020/04/05 Python
Keras自动下载的数据集/模型存放位置介绍
2020/06/19 Python
高品质和独特的产品世界:Creations and Collections
2018/01/07 全球购物
环保建议书200字
2014/05/14 职场文书
酒店管理失职检讨书
2014/09/16 职场文书
2014年度个人总结范文
2015/03/09 职场文书
2015年大学辅导员工作总结
2015/05/12 职场文书
小学四年级作文之写景
2019/08/23 职场文书
JAVA API 实用类 String详解
2021/10/05 Java/Android
ajax请求前端跨域问题原因及解决方案
2021/10/16 Javascript
配置nginx负载均衡
2022/05/06 Servers