本文始发于我的公众号:TechFlow,原创不易,求个关注web
今天是Python专题的第9篇文章,咱们来聊聊Python的函数式编程与闭包。编程
函数式编程这个概念咱们可能或多或少都据说过,刚据说的时候不明觉厉,以为这是一个很是黑科技的概念。可是实际上它的含义很朴实,可是延伸出来许多丰富的用法。数组
在早期编程语言还不是不少的时候,咱们会将语言分红高级语言与低级语言。好比汇编语言,就是低级语言,几乎什么封装也没有,作一个赋值运算还须要咱们手动调用寄存器。而高级语言则从这些面向机器的指令当中抽身出来,转而面向过程或者是对象。也就是说咱们写代码面向的是一段计算过程或者是一个计算机当中抽象出来的对象。若是你学过面向对象,你会发现和面向过程相比,面向对象的抽象程度更高了一些,作了更加完善的封装。闭包
在面向对象以后呢,咱们还能够作什么封装和抽象呢?这就轮到了函数式编程。app
函数咱们都了解,就是咱们定义的一段程序,它的输入和输出都是肯定的。咱们把一段函数写好,它能够在任何地方进行调用。既然函数这么好用,那么能不能把函数也当作是一个变量进行返回和传参呢?框架
OK,这个就是函数式编程最直观的特色。也就是说咱们写的一段函数也能够做为变量,既能够用来赋值,还能够用来传递,而且还能进行返回。这样一来,大大方便了咱们的编码,可是这并非有利无害的,相反它带来许多问题,最直观的问题就是因为函数传入的参数还能够是另外一个函数,这会致使函数的计算过程变得不可肯定,许多超出咱们预期的事情都有可能发生。编程语言
因此函数式编程是有利有弊的,它的确简化了许多问题,但也产生了许多新的问题,咱们在使用的过程中须要谨慎。编辑器
在咱们以前介绍filter、map、reduce以及自定义排序的时候,其实咱们已经用到了函数式编程的概念了。函数式编程
好比在咱们调用sorted进行排序的时候,若是咱们传入的是一个对象数组,咱们但愿根据咱们制定的字段排序,这个时候咱们每每须要传入一个匿名函数,用来制定排序的字段。其实传入的匿名函数,其实就是函数式编程最直观的体现了:函数
sorted(kids, key=lambda x: x['score'])
复制代码
除此以外,咱们还能够返回一个函数,好比咱们来看一个例子:
def delay_sum(nums):
def sum():
s = 0
for i in nums:
s += i
return s
return sum
复制代码
若是这个时候咱们调用delay_sum传入一串数字,咱们会获得什么?
答案是一个函数,咱们能够直接输出,从打印信息里看出这一点:
>>> delay_sum([1, 3, 4, 2])
<function delay_sum.<locals>.sum at 0x1018659e0>
复制代码
咱们想得到这个运算结果应该怎么办呢?也很简单,咱们用一个变量去接收它,而后执行这个新的变量便可:
>>> f = delay_sum([1, 3, 4, 2])
>>> f()
10
复制代码
这样作有一个好处是咱们能够延迟计算,若是不使用函数式编程,那么咱们须要在调用delay_sum这个函数的时候就计算出结果。若是这个运算量很小还好,若是这个运算量很大,就会形成开销。而且当咱们计算出结果来以后,这个结果也许不是当即使用的,可能到很晚才会用到。既然如此,咱们返回一个函数代替了运算,当后面真正须要用到的时候再执行结果,从而延迟了运算。这也是不少计算框架的经常使用思路,好比spark。
咱们再来回顾一下咱们刚才举的例子,在刚才的delay_sum函数当中,咱们内部实现了一个sum函数,咱们在这个函数当中调用了delay_sum函数传入的参数。这种对外部做用域的变量进行引用的内部函数就称为闭包。
其实这个概念很形象,由于这个函数内部调用的数据对于调用方来讲是封闭的,彻底是一个黑盒,除非咱们查看源码,不然咱们是不知道它当中数据的来源的。除了不知道来源以外,更重要的是它引用的是外部函数的变量,既然是变量就说明是动态的。也就是说咱们能够经过改变某些外部变量的值来改变闭包的运行效果。
这么说有点拗口,咱们来看一个简单的例子。在Python当中有一个函数叫作math.pow其实就是计算次方的。好比咱们要计算x的平方,那么咱们应该这样写:
math.pow(x, 2)
复制代码
可是若是咱们当前场景下只须要计算平方,咱们每次都要传入额外再传入一个2会显得很是麻烦,这个时候咱们使用闭包,能够简化操做:
def mypow(num):
def pw(x):
return math.pow(x, num)
return pw
pow2 = mypow(2)
print(pow2(10))
复制代码
经过闭包,咱们把第二个变量给固定了,这样咱们只须要使用pow2就能够实现原来math.pow(x, 2)的功能了。若是咱们忽然需求变动须要计算3次方或者是4次方,咱们只须要修改mypow的传入参数便可,彻底不须要修改代码。
实际上这也是闭包最大的使用场景,咱们能够经过闭包实现一些很是灵活的功能,以及经过配置修改一些功能等操做,而再也不须要经过代码写死。要知道对于工业领域来讲,线上的代码是不能随便变动的,尤为是客户端,好比apple store或者是安卓商店当中的软件包,只有用户手动更新才会拉取。若是出现问题了,几乎没有办法修改,只能等用户手动更新。因此常规操做就是使用一些相似闭包的灵活功能,经过修改配置的方式改变代码的逻辑。
除此以外闭包还有一个用处是能够暂存变量或者是运行时的环境。
举个例子,咱们来看下面这段代码:
def step(x=0):
x += 5
return x
复制代码
这是没有使用闭包的函数,无论咱们调用多少次,答案都是5,执行完x+=5以后的结果并不会被保存起来,当函数返回了,这个暂存的值也就被抛弃了。那若是我但愿每次调用都是依据上次调用的结果,也就是说咱们每次修改的操做都能保存起来,而不是丢弃呢?
这个时候就须要使用闭包了:
def test(x=0):
def step():
nonlocal x
x += 5
return x
return step
t = test()
t()
>>> 5
t()
>>> 10
复制代码
也就是说咱们的x的值被存储起来了,每次修改都会累计,而不是丢弃。这里须要注意一点,咱们用到了一个新的关键字叫作nonlocal,这是Python3当中独有的关键字,用来申明当前的变量x不是局部变量,这样Python解释器就会去全局变量当中去寻找这个x,这样就能关联上test方法当中传入的参数x。Python2官方已经不更新了,不推荐使用。
因为在Python当中也是一切都是对象,若是咱们把闭包外层的函数当作是一个类的话,其实闭包和类区别就不大了,咱们甚至能够给闭包返回的函数关联函数,这样几乎就是一个对象了。来看一个例子:
def student():
name = 'xiaoming'
def stu():
return name
def set_name(value):
nonlocal name
name = value
stu.set_name = set_name
return stu
stu = student()
stu.set_name('xiaohong')
print(stu())
复制代码
最后运算的结果是xiaohong,由于咱们调用set_name改变了闭包外部的值。这样固然是能够的,可是通常状况下咱们并不会用到它。和写一个class相比,经过闭包的方法运算速度会更快。缘由比较隐蔽,是由于闭包当中没有self指针,从而节省了大量的变量的访问和运算,因此计算的速度要快上一些。可是闭包搞出来的伪对象是不能使用继承、派生等方法的,并且和正常的用法格格不入,因此咱们知道有这样的方法就能够了,现实中并不会用到。
闭包虽然好用,可是不当心的话也是很容易踩坑的,下面介绍几个常见的坑点。
这一点咱们刚才已经提到了,在闭包当中咱们不能直接访问外部的变量的,必需要经过nonlocal关键字进行标注,不然的话是会报错的。
def test():
n = 0
def t():
n += 5
return n
return t
复制代码
好比这样的话,就会报错:
闭包有一个很大的问题就是不能使用循环变量,这个坑藏得很深,由于单纯从代码的逻辑上来看是发现不了的。也就是说逻辑上没问题的代码,运行的时候每每会出乎咱们的意料,这须要咱们对底层的原理有深入地了解才能发现,好比咱们来看一个例子:
def test(x):
fs = []
for i in range(3):
def f():
return x + i
fs.append(f)
return fs
fs = test(3)
for f in fs:
print(f())
复制代码
在上面这个例子当中,咱们使用了for循环来建立了3个闭包,咱们使用fs存储这三个闭包并进行返回。而后咱们经过调用test,来得到了这3个闭包,而后咱们进行了调用。
这个逻辑看起来应该没有问题,按照道理,这3个闭包是经过for循环建立的,而且在闭包当中咱们用到了循环变量i。那按照咱们的想法,最终输出的结果应该是[3, 4, 5],可是很遗憾,最后咱们获得的结果是[5, 5, 5]。
看起来很奇怪吧,其实一点也不奇怪,由于循环变量i并非在建立闭包的时候就set好的。而是当咱们执行闭包的时候,咱们再去寻找这个i对应的取值,显然当咱们运行闭包的时候,循环已经执行完了,此时的i停在了2。因此这3个闭包的执行结果都是2+3也就是5。这个坑是由Python解释器当中对于闭包执行的逻辑致使的,咱们编写的逻辑是对的,可是它并不按照咱们的逻辑来,因此这一点要千万注意,若是忘记了,想要经过debug查找出来会很难。
虽然从表面上闭包存在一些问题和坑点,可是它依然是咱们常用的Python高级特性,而且它也是不少其余高级用法的基础。因此咱们理解和学会闭包是很是有必要的,千万不能因噎废食。
其实并不仅是闭包,不少高度抽象的特性都或多或少的有这样的问题。由于当咱们进行抽象的时候,咱们当然简化了代码,增长了灵活度,但与此同时咱们也让学习曲线变得陡峭,带来了更多咱们须要理解和记住的内容。本质上这也是一个trade-off,好用的特性须要付出代码,易学易用的每每意味着比较死板不够灵活。对于这个问题,咱们须要保持心态,不过好在初看时也许有些难以理解,但整体来讲闭包仍是比较简单的,我相信对大家来讲必定不成问题。
好了,今天的文章就是这些,若是以为有所收获,请顺手点个关注或者转发吧,大家的举手之劳对我来讲很重要。