美文网首页
python玄学系列(第二集):这大概是最全面最通俗易懂的pyt

python玄学系列(第二集):这大概是最全面最通俗易懂的pyt

作者: Nstream | 来源:发表于2020-01-13 15:24 被阅读0次

    ​写在前面

    鉴于天下苦 “python闭包” 久矣,今天,就由我给你们详细解释一下,百分百包教包会,童叟无欺,若看完还是不会,那你也拿我没什么办法!!!

    正文

    要想明白闭包,嵌套函数是迈不过去了,所以,我先讲解一下这个知点。

    先来看看百度百科对嵌套函数的定义

    1、嵌套函数,就是指在某些情况下,您可能需要将某函数作为另一函数的参数使用,这一函数就是嵌套函数。

    2、嵌套函数(Nested function)是在另一个函数(即:封闭函数)中定义的函数

    对于嵌套函数,大致有三类用途:

    数据隐藏
    DRY 原则
    闭包

    1、封装 - 数据隐藏

    保护一部分代码不受函数外部变化的影响,从全局作用域中隐藏起来
    示例:

    def a(n):
        def b(m):
           return m+1
        add_one = b(n)
    print(add_one)
    

    调用运行结果:

    >>>a(1)
    2
    >>>b(2)
    NameError: name 'b' is not defined
    

    可以看到,外部无法访问函数b,这就起到了数据隐藏的效果。

    打个通俗的比方,龟儿子和龟爸爸玩捉迷藏,龟儿子躲进柜子,龟爸爸在外面是看不到它的龟儿子的,唯一的办法就是,打开柜子,大叫一声:“龟儿子,给我滚粗来”,然后龟儿子就真的就像个龟儿子一样慢吞吞的爬出来了。

    2、DRY 原则

    DRY(Don’t Repeat Yourself)- 是指在程序设计以及计算中避免重复代码,因为这样会降低灵活性、简洁性,并且有可能导致代码之间的矛盾。

    DRY 更多的是一种架构设计思想,在软件开发过程中的万事万物均可能复,大到标准、框架、开发流程;中到组件、接口;小到功能、代码均纯存在自我重复。而 DRY 提倡的就是在软件开发过程中应消除所有这些自我重复。

    来看一个例子:

    例如,处理文件时,需要支持打开的文件对象和文件名,传统方式是采用两个函数分别实现。

    (1)文件对象的方式读取
    def read_file_object(f):
        for line in f:
          print(line)
    ​
    ​
    ​
    ​
    f = open('a.txt', 'r', encoding='utf8')
    read_file_object(f)
    

    结果为:

    我是蔡徐坤
    
    (2)以文件名的方式读取
    def read_file_name(file_name):
      if isinstance(file_name, str):
        with open(file_name, 'r', encoding='utf8') as f:
          for line in f:
            print(line)
    ​
    ​
    ​
    ​
    read_file_name('a.txt')
    

    结果为:

    我是蔡徐坤
    

    这种方式显然不好,不同的读取方式需要调用不同的函数,并且读取文件的代码重复了,显得冗余啰嗦,来看看嵌套函数怎么优雅的实现这个功能吧:

    def read_file(file):
      def inner(file_):
        for line in file_:
          print(line, end='')
    ​
    ​
      if isinstance(file, str):
        with open(file, 'r', encoding='utf8') as f:
          inner(f)
      else:
        inner(file)
    ​
    ​
    ​
    ​
    read_file("a.txt")
    

    看吧,一个嵌套函数实现了所有功能,是不是很pythonic,代码不仅优雅,并且简单,适用行强,也符合了DRY原则

    3、闭包

    今天的主角就这样闪亮登场了(前排鼓掌)

    我们先看看维基百科中对闭包的解释:

    在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

    看看就行了,刚学闭包别想着能看懂它,因为它说的就不是人话。下面就让我一步一步把这句话翻译成人话给你听(手动狗头)

    公主号: “暮秋梵星”    后台有大量资料给你
    

    开始之前,为了避免像我一样的数学专业生引起误会,区别一下数学中的闭包,同样,来看一下数学中的闭包的概念:

    数学中,若对某个集合的成员进行一种运算,生成的仍然是这个集合的成员,则该集合被称为在这个运算下闭合。当一个集合 S 在某个运算下不闭合的时候,我们通常可以找到包含 S 的最小的闭合集合。这个最小闭合集合被称为 S 的(关于这个运算的)闭包。

    我知道你看不懂,放出来也不是给你看的,知道他俩不是一个东西就行了,如果您非要问我这两个闭包有什么关系,很简单,就像java和javascript的关系。什么?你还是看不懂?给你一张图片感受一下

    在这里插入图片描述

    下面正式开始闭包的讲解,不然你们要说我在开始忽悠了

    (1)作用域

    在python中,一个py文件称之为一个模块,在模块最外层定义的变量称之为全局变量,作用域是整个模块,定义在函数内部的变量称之为菊部变量,不对,是局部变量(别想歪了,好好听课),它的作用域是整个函数。

    num = 1 # 全局作用域变量
    def a():
      print(num)
    

    这里的num是全局变量,函数外部是能访问到的

    def b():
      num = 2
    print(num)
    

    这里的num是局部变量,函数外部是不能访问的,如果你运行程序,会报错:
    NameError: name 'num' is not defined

    (2)闭包的本质

    先让我们来看一下这段代码:

    def outer():
      arg_1 = "我是外部函数的变量"
    ​
    ​
      def inner():
        print(arg_1)
      inner()
    ​
    ​
    outer()
    

    运行一下:

    我是外部函数的变量
    

    这里的arg_1是outer函数的局部变量,当我们调用outer函数的时候,内部函数inner也会被调用,arg_1的值被访问到并且打印出来,而当outer函数被调用结束过后,arg_1变量所占用的内存空间会被释放,就相当于从这个世界消失了,任何地方想要访问都不可能。这还不是闭包,但是已经离闭包很近了。

    没看懂?别急,继续看下去,你就明白了。

    现在,我们把上面的代码的某一行变一下:

    def outer():
      arg_1 = "我是外部函数的变量"
    ​
    ​
      def inner():
        print(arg_1)
      return inner
    ​
    ​
    a = outer()
    a()
    

    结果为:

    我是外部函数的变量
    

    仔细看看有什么区别,你会发现,我们把inner函数的引用,作为outer函数的返回值了,意思就是说,outer函数的返回值也是一个函数,这个函数就是内部函数inner。这,就是闭包。

    看到这里,估计你会骂我,说的是什么屁话,这有个锤子区别啊!

    安静!让我慢慢解答你的疑点

    你仔细看一下代码,会发现一个很奇怪的现象,a = outer()这行代码已经调用了outer函数,也就是说,outer函数已经执行完毕了,那是不是arg_1变量也应该随之消失呢,可是,当我执行a()这句代码时,发现还能打印出arg_1变量的值,这是违反常理的。这里面的关键就在于inner函数被作为返回值了,并且在inner函数中引用了引用了arg_1变量,把变量值inner函数包在了一起,也就是我们所说的闭包,这样,arg_1变量就脱离了原本创建它的函数(outer函数)而存在。

    好了,这就是闭包的本质,你应该能看懂了吧。

    但这样你又会问了,这个闭包有球用啊!

    大有用处,有了闭包,我们可以不使用全局变量,要知道,在任何一门语言中,全局变量都是被谨慎使用了,稍不注意,就会出问题。还有一个很大的用处,把一些数据和函数联系起来,这大大简化了代码,也提升了可读性。比如, 你想要实现这样的功能,可以用类来实现:

    class Test():
      def __init__(self, a):
        self.a = a
    ​
    ​
      def add(self, b):
        print(self.a + b)
    ​
    ​
    test = Test(1)
    test.add(2)
    test.add(3)
                
    

    结果为:

    3
    4
    

    有木有感觉一个简单的功能还得用类来实现,很麻烦,看着也不舒服,看看闭包怎么不用类实现的:

    def outer(a):
      def inner(b):
        print(a+b)
      return inner
    ​
    ​
    test = outer(1)
    test(2)
    test(3)
    

    看吧,是不是很简单呢!

    这里,我再深入剖析一下闭包中,外部函数里定义的变量为什么能脱离外部函数而存在。

    其实所有的函数,都有一个closure属性,如果这个函数是一个闭包的话,那么它返回的是一个由 cell 对象 组成的元组对象。cell 对象的cell_contents 属性就是闭包中的自由变量。

    >>>test.__closure__
    (<cell at 0x000001C388850A68: int object at 0x0000000074E96C40>,)
    >>>test.__closure__[0].cell_contents
    1
    

    这下看明白了吧,其实也就是python把这个变量存起来了。

    回过头来,我们再看一下维基百科对闭包的解释,就so easy了。

    在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。

    闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

    归纳一下,闭包必须满足的三个条件:

    1、需要有一个内嵌函数(也就是上面例子中的inner函数)

    2、内嵌函数需要引用定义在外部函数中变量(自由变量)

    3、内嵌函数需要被返回(这一点最重要,是区别普通嵌套函数与闭包的本质)

    好了,今天的内容就全部结束了,其实闭包还有一个最重要的用途,那就是“装饰器”,这一点估计得再写一篇文章,大家敬请期待!!!

    相关文章

      网友评论

          本文标题:python玄学系列(第二集):这大概是最全面最通俗易懂的pyt

          本文链接:https://www.haomeiwen.com/subject/fytnactx.html