今天是Python专题的第9篇文章,我们来聊聊Python的函数式编程与闭包。


函数式编程


函数式编程这个观点我们可能或多或少都听说过,刚听说的时刻不明觉厉,以为这是一个异常黑科技的观点。然则实际上它的寄义很朴素,然则延伸出来许多厚实的用法。

在早期编程语言还不是许多的时刻,我们会将语言分成高级语言与低级语言。好比汇编语言,就是低级语言,险些什么封装也没有,做一个赋值运算还需要我们手动挪用寄存器。而高级语言则从这些面向机械的指令当中抽身出来,转而面向历程或者是工具。也就是说我们写代码面向的是一段盘算历程或者是一个盘算机当中抽象出来的工具。若是你学过面向工具,你会发现和面向历程相比,面向工具的抽象水平更高了一些,做了加倍完善的封装。

在面向工具之后呢,我们还可以做什么封装和抽象呢?这就轮到了函数式编程。

函数我们都领会,就是我们界说的一段程序,它的输入和输出都是确定的。我们把一段函数写好,它可以在任何地方举行挪用。既然函数这么好用,那么能不能把函数也看成是一个变量举行返回和传参呢?

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([1342])
<function delay_sum.<locals>.sum at 0x1018659e0>

我们想获得这个运算效果应该怎么办呢?也很简朴,我们用一个变量去吸收它,然后执行这个新的变量即可:

>>> f = delay_sum([1342])
>>> f()
10

这样做有一个利益是我们可以延迟盘算,若是不使用函数式编程,那么我们需要在挪用delay_sum这个函数的时刻就盘算出效果。若是这个运算量很小还好,若是这个运算量很大,就会造成开销。而且当我们盘算出效果来之后,这个效果也许不是立刻使用的,可能到很晚才会用到。既然如此,我们返回一个函数取代了运算,当后面真正需要用到的时刻再执行效果,从而延迟了运算。这也是许多盘算框架的常用思绪,好比spark


闭包


我们再来回首一下我们适才举的例子,在适才的delay_sum函数当中,我们内部实现了一个sum函数,我们在这个函数当中挪用了delay_sum函数传入的参数。这种对外部作用域的变量举行引用的内部函数就称为闭包

,

Allbet注册ALLbet6.com

欢迎进入Allbet注册(Allbet Game):www.aLLbetgame.us,欧博官网是欧博集团的官方网站。欧博官网开放Allbet注册、Allbe代理、Allbet电脑客户端、Allbet手机版下载等业务。

,

实在这个观点很形象,由于这个函数内部挪用的数据对于挪用方来说是封锁的,完全是一个黑盒,除非我们查看源码,否则我们是不知道它当中数据的泉源的。除了不知道泉源之外,更主要的是它引用的是外部函数的变量,既然是变量就说明是动态的。也就是说我们可以通过改变某些外部变量的值来改变闭包的运行效果

这么说有点拗口,我们来看一个简朴的例子。在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,好用的特征需要支出代码,易学易用的往往意味着对照死板不够天真。对于这个问题,我们需要保持心态,不外幸亏初看时也许有些难以明白,但总体来说闭包照样对照简朴的,我信赖对你们来说一定不成问题。