美文网首页
Python | 对于变量作用域和闭包(Closure)的理解

Python | 对于变量作用域和闭包(Closure)的理解

作者: Producer | 来源:发表于2018-03-24 20:46 被阅读0次

    Python中的变量作用域,这个主题来源于最近一个实际的需求,对于任意深度的字典进行解析。
    初步一看,这个问题很明了,递归解析,直到最后一层结束。实际也确实是这么做的,但是在实现上却遇到了一些问题,所以正好就函数变量的作用域谈一下自己的理解,顺便再学习一下闭包。

    问题引入

    我们的问题是这样的,有两个家庭,一对异性恋TOM和JANE组成的家庭和一只单身狗DOG一个人组成的家庭。他们都有姓名和年龄的属性,现在我们需要对于他们的年龄和姓名进行解析。故事的主人公们长这样:

    to_be_parsed_dict = {
        "Couple": {
            "Husband": {"name": "Tom", "age": 15},
            "Wife": {"name": "Jane", "age": 43}
        },
        "Single": {"name": "Dog", "age": 34}
    }
    

    任意深度的字典的解析


    • 第一个需求:解析这两个家庭的所有人的姓名组成列表

    毫无疑问,递归,因为需求比较简单,所以就找到最后一层字典中有"name"结束即可。

    def get_name(src_dict, result_list):
        for key in src_dict:
            if src_dict.__contains__("name"):
                result_list.append(src_dict["name"])
                break
            else:
                get_name(src_dict[key], result_list)
        return result_list
    

    解析一下故事的主人公们,得到了三位主角的名字,emmmm,一切看起来不错。

    >>> result_list = []
    >>> get_name(to_be_parsed_dict, result_list)
    ['Tom', 'Jane', 'Dog']
    

    • 第二个需求:看一下这两个家庭的人加起来超没超过100岁

    看起来和第一个需求完全一样,那我们直接重复一下代码,毕竟实现的时候copy yourself是最爽的(您的好友“维护”已经加入游戏)。

    def get_age(src_dict, total_age):
        for key in src_dict:
            if src_dict.__contains__("age"):
                total_age += src_dict["age"]
                break
            else:
                get_age(src_dict[key], total_age)
        return total_age
    

    完全一样的思路,代码都几乎一样,应该可以安心拍胸脯了,作为一个负责的码农,还是假装测试一下吧。

    >>> total_age = 0
    >>> get_age(to_be_parsed_dict, total_age)
    0
    

    emmmm,肯定不是代码出问题了,肯定是操作系统有点问题,重启试试。
    结果还是一样。

    为什么?
    好像要谈到函数变量的作用域的理解了。
    第一个需求和第二个需求其唯一的差别就在于:第一个需求传了可变类型list,第二个需求传了不可变类型int。问题不在于变量类型本身,而是因为我们在二个函数中用到了total_age += src_dict["age"],这个句法其本质是total_age = total_age + src_dict["age"],这就相当于在函数的内部重新分配了变量,变量的作用域变成了函数内有效,自然就无法对于函数外部作用域的变量产生任何影响了。所以结果输出的还是函数外部在定义的时候的赋值0。
    而在第一个函数中,写法是result_list.append(src_dict["name"]),并没有在函数的内部重新非配变量。如果我们在函数的内部加上result_list = [],那么其结果显然是无法正常工作的。

    为了解决这个问题,我们可以引入全局变量,更新后的代码如下:

    def get_age(src_dict):
        global total_age
        for key in src_dict:
            if src_dict.__contains__("age"):
                total_age += src_dict["age"]
                break
            else:
                get_age(src_dict[key])
        return total_age
    

    这里在命令行里面并没有跑起来,不过通过了脚本内的测试(要在这里挖坑了,也许命令行里不能定义全局变量)

    闭包

    既然函数的变量是有作用域的,那么函数中函数的变量的作用域又怎么说呢?


    • 第三个需求:我想要将这两个家庭的人的参数解析两遍

    对于姓名的解析,我们可以将第一次解析得到的列表作为参数传递函数的第二次调用,对于年龄的解析,由于是全局变量,所以直接执行两次函数即可。
    但是这样的做法是不安全的,服务端对于客户端产生了依赖,这破坏了服务端的封装,比如说不小心在函数的某处改变了全局变量的值,那么重复解析两次得到的结果是不准确的。
    如此,便到了闭包出场的时候,直接上代码。

    def name_getter():
        result_list = []
        def get_name(src_dict):
            for key in src_dict:
                if src_dict.__contains__("name"):
                    result_list.append(src_dict["name"])
                    return result_list
                else:
                    get_name(src_dict[key])
            return result_list
        return get_name
    
    
    def age_getter():
        total_age = 0
        def get_age(src_dict):
            nonlocal total_age
            for key in src_dict:
                if src_dict.__contains__("age"):
                    total_age += src_dict["age"]
                    break
                else:
                    get_age(src_dict[key])
            return total_age
        return get_age
    

    这两个函数的核心是在实际的执行函数层外包了一层函数,用来维持数据的状态。
    需要特别注意age_getternonlocal total_age这个声明,如同在第二个需求中讨论的,为了避免get_age函数中重新声明变量造成外部声明的变量失效,需要用nonlocal关键字来声明。相对于gobal声明来说,有效控制了变量名的有效范围,是有助于保持数据不被污染的。
    这两个函数的实际执行像这样。

    >>> re_get_name = name_getter()
    >>> re_get_name(to_be_parsed_dict)
    ['Tom', 'Jane', 'Dog']
    >>> re_get_name(to_be_parsed_dict)
    ['Tom', 'Jane', 'Dog', 'Tom', 'Jane', 'Dog']
    
    >>> re_get_age = age_getter()
    >>> re_get_age(to_be_parsed_dict)
    92
    >>> re_get_age(to_be_parsed_dict)
    184
    

    为什么函数的上次执行结果可以保存。
    一方面,因为外层函数返回的是内层函数,变量的初始化在外层函数的初始化过程中完成,以后每次调用外层函数,实质是调用了内层函数,所以类似totoal_age=0的赋值只会被执行一次,不会被覆盖。
    另一方面,外层函数一直存在(一个python对象),所以数据可以保持。


    • 对于闭包的一点理解

    从目前我写的代码的角度来看,闭包并不是非常有用的特性,所以对于概念本身也谈不上特别熟悉,但我认为闭包从本质来说还是函数变量作用域的问题。

    从功能的角度来看。
    第一点,闭包有效的减少了参数的数量,在实现第三个需求的时候,实际看到的只需要传递一个参数就可以实现解析了。
    第二点,闭包可以维持内部状态,这也是为什么可以连续解析两次的原因。

    从形式的角度来看。
    这个写法非常非常像装饰器的写法,二者之间必然存在联系。不过现在我也说不出来联系在哪里。

    从实用的角度来看。
    这个找了资料,原文在这里 Python深入04 闭包
    闭包有效的减少了函数所需定义的参数数目。这对于并行运算来说有重要的意义。在并行运算的环境下,我们可以让每台电脑负责一个函数,然后将一台电脑的输出和下一台电脑的输入串联起来。最终,我们像流水线一样工作,从串联的电脑集群一端输入数据,从另一端输出数据。这样的情境最适合只有一个参数输入的函数。闭包就可以实现这一目的。

    并行运算正称为一个热点。这也是函数式编程又热起来的一个重要原因。函数式编程早在1950年代就已经存在,但应用并不广泛。然而,我们上面描述的流水线式的工作并行集群过程,正适合函数式编程。由于函数式编程这一天然优势,越来越多的语言也开始加入对函数式编程范式的支持。

    相关文章

      网友评论

          本文标题:Python | 对于变量作用域和闭包(Closure)的理解

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