本文始发于个人公众号:TechFlow,原创不易,求个关注

今天是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([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,好用的特性需要付出代码,易学易用的往往意味着比较死板不够灵活。对于这个问题,我们需要保持心态,不过好在初看时也许有些难以理解,但总体来说闭包还是比较简单的,我相信对你们来说一定不成问题。

好了,今天的文章就是这些,如果觉得有所收获,请顺手点个关注或者转发吧,你们的举手之劳对我来说很重要。

Python——五分钟理解函数式编程与闭包的更多相关文章

  1. Python——五分钟理解元类(metaclasses)

    “元类的魔幻变化比 99% 的用户所担心的更多,当你搞不懂是否真的需要用它的时候,就是不需要.” —Tim Peters 本文源于在 PyCon UK 2008 上的一个快速演讲. 元类被称为 Pyt ...

  2. Python函数式编程之闭包

    -------------------------函数式编程之*******闭包------------------------ Note: 一:简介 函数式编程不是程序必须要的,但是对于简化程序有很 ...

  3. (转)每天进步一点点——五分钟理解一致性哈希算法(consistent hashing)

    背景:在redis集群中,有关于一致性哈希的使用. 一致性哈希:桶大小0~(2^32)-1 哈希指标:平衡性.单调性.分散性.负载性 为了提高平衡性,引入“虚拟节点” 每天进步一点点——五分钟理解一致 ...

  4. 理解函数式编程中的函数组合--Monoids(二)

    使用函数式语言来建立领域模型--类型组合 理解函数式编程语言中的组合--前言(一) 理解函数式编程中的函数组合--Monoids(二) 继上篇文章引出<范畴论>之后,我准备通过几篇文章,来 ...

  5. 带你五分钟了解python的函数式编程与闭包

    前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:梁唐 PS:如有需要Python学习资料的小伙伴可以加点击下方链接自行 ...

  6. Python进阶【第五篇】函数式编程及某些特殊函数

    一.函数式编程——Functional Programming 函数式=编程语言定义的函数+数学意义的函数 在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语 ...

  7. (转) 站在C#和JS的角度细谈函数式编程与闭包

    1.函数式编程是什么? 摘自百度的说法是.函数式编程是种编程典范,它将电脑运算视为函数的计算.函数编程语言最重要的基础是 λ 演算(lambda calculus).而且λ演算的函数可以接受函数当作输 ...

  8. Python修饰器的函数式编程

    Python的修饰器的英文名叫Decorator,当你看到这个英文名的时候,你可能会把其跟Design Pattern里的Decorator搞混了,其实这是完全不同的两个东西.虽然好像,他们要干的事都 ...

  9. 小白的Python之路 day3 函数式编程,高阶函数

    函数式编程介绍   函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的 ...

随机推荐

  1. Python - 超好用的第三方库pathlib,快速获取项目中各种路径

    前言 之前曾介绍过Python的os库详细使用方式,具体可看看这篇博文:https://www.cnblogs.com/poloyy/p/12341231.html 博主在学完os库之后,就开始投入使 ...

  2. 将url转化成file文件

            let img = "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=508387608,28489740 ...

  3. Webpack和Gulp,Webpack和Gulp的基本区别:

    Gulp和Webpack的基本区别: gulp可以进行js,html,css,img的压缩打包,是自动化构建工具,可以将多个js文件或是css压缩成一个文件,并且可以压缩为一行,以此来减少文件体积,加 ...

  4. PageRank 算法初步了解

    前言 因为想做一下文本自动摘要,文本自动摘要是NLP的重要应用,搜了一下,有一种TextRank的算法,可以做文本自动摘要.其算法思想来源于Google的PageRank,所以先把PageRank给了 ...

  5. vue2.0:项目开始,首页入门(main.js,App.vue,importfrom)

    对main.js App.vue 等进行操作: 但是这就出现了一个问题:什么是main.js,他主要干什么用的?App.vue又是干什么用的?main.js 里面的import from又在干嘛?ne ...

  6. (转)浅析epoll-为何多路复用I/O要使用epoll

    原文地址:http://www.cppfans.org/1417.html 浅析epoll-为何多路复用I/O要使用epoll 现如今,网络通讯中用epoll(linux)和IOCP(windows) ...

  7. 深入理解计算机系统 (CS:APP) Lab2 - Bomb Lab 解析

    原文地址:https://billc.io/2019/04/csapp-bomblab/ 写在前面 CS:APP是这学期的一门硬核课程,应该是目前接触到最底层的课程了.学校的教学也是尝试着尽量和CMU ...

  8. WPF 启动缓慢问题

    Actually there's 2 main reasons that the default project type for WPF applications is x86. Intellitr ...

  9. 手动生成WebService代理类

    方式一: 手动生成WebService代理类需要把一句生成语句,如 wsdl.exe /l:cs /out:D:/ProxyServices.cs http://localhost/WebServic ...

  10. ALSA driver---DPCM

    https://www.kernel.org/doc/html/v4.11/sound/soc/dpcm.html Description Dynamic PCM allows an ALSA PCM ...