zoukankan      html  css  js  c++  java
  • 流畅的python 闭包

    闭包

    人们有时会把闭包和匿名函数弄混。这是有历史原因的:在函数内部定义函数不常见,直到开始使用匿名函数才会这样做。而且,只有涉及嵌套函数时才有闭包问题。因此,很多人是同时知道这两个概
    念的。其实,闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
    这个概念难以掌握,最好通过示例理解。假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。起初,avg 是这样使用的:

    >>> avg(10)
    10.0
    >>> avg(11)
    10.5
    >>> avg(12)
    11.0

    avg 从何而来,它又在哪里保存历史值呢?
    初学者可能会像示例那样使用类实现。
    示例average_oo.py:计算移动平均值的类

    class Averager():
        def __init__(self):
            self.series = []
        def __call__(self, new_value):
            self.series.append(new_value)
            total = sum(self.series)
            return total/len(self.series)

    Averager 的实例是可调用对象:

    >>> avg = Averager()
    >>> avg(10)
    10.0
    >>> avg(11)
    10.5
    >>> avg(12)
    11.0

    使用高阶函数 make_averager

    def make_averager():
        series = []
        def averager(new_value):
            series.append(new_value)
            total = sum(series)
            return total/len(series)
        return averager

    调用 make_averager 时,返回一个 averager 函数对象。每次调用
    averager 时,它会把参数添加到系列值中,然后计算当前平均值,如示例

    >>> avg = make_averager()
    >>> avg(10)
    10.0
    >>> avg(11)
    10.5
    >>> avg(12)
    11.0

    注意,这两个示例有共通之处:调用 Averager() 或make_averager() 得到一个可调用对象 avg,它会更新历史值,然后计算当前均值。在示例 1中,avg 是 Averager 的实例;在示例 2
    中是内部函数 averager。不管怎样,我们都只需调用 avg(n),把 n放入系列值中,然后重新计算均值。Averager 类的实例 avg 在哪里存储历史值很明显:self.series 实例属性。但是第二个示例中的 avg 函数在哪里寻找 series 呢?注意,series 是 make_averager 函数的局部变量,因为那个函数的定义体中初始化了 series:series = []。可是,调用 avg(10)时,make_averager 函数已经返回了,而它的本地作用域也一去不复返了。在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量,参见图 

    图 7-1:averager 的闭包延伸到那个函数的作用域之外,包含自由变量 series 的绑定审查返回的 averager 对象,我们发现 Python 在 __code__ 属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称,如示例2所示。审查 make_averager创建的函数

    >>> avg.__code__.co_varnames
    ('new_value', 'total')
    >>> avg.__code__.co_freevars
    ('series',)

    series 的绑定在返回的 avg 函数的 __closure__ 属性中。avg.__closure__ 中的各个元素对应于avg.__code__.co_freevars 中的一个名称。这些元素是 cell 对象,
    有个 cell_contents 属性,保存着真正的值。这些属性的值如示例3

    >>> avg.__code__.co_freevars
    ('series',)
    >>> avg.__closure__
    (<cell at 0x107a44f78: list object at 0x107a91a48>,)
    >>> avg.__closure__[0].cell_contents
    [10, 11, 12]

    综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。

    nonlocal声明

    前面实现 make_averager 函数的方法效率不高。在示例 1/2中,我们把所有值存储在历史数列中,然后在每次调用 averager 时使用 sum 求
    和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。示例 4 中的实现有缺陷,只是为了阐明观点。你能看出缺陷在哪儿
    吗?示例 4 计算移动平均值的高阶函数,不保存所有历史值,但有缺陷

    def make_averager():
        count = 0
        total = 0
        def averager(new_value):
            count += 1
            total += new_value
            return total / count
        return averager

    尝试使用示例 4中定义的函数,会得到如下结果:

    >>> avg = make_averager()
    >>> avg(10)
    Traceback (most recent call last):
    ...
    UnboundLocalError: local variable 'count' referenced before assignment
    >>>

    问题是,当 count 是数字或任何不可变类型时,count += 1 语句的作用其实与 count = count + 1 一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。total 变量也受
    这个问题影响。
    示例 1/2没遇到这个问题,因为我们没有给 series 赋值,我们只是调用 series.append,并把它传给 sum 和 len。也就是说,我们利用了列表是可变的对象这一事实。
    但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如 count = count + 1,其实会隐式创建局部变量 count。这样,count 就不是自由变量了,因此不会保存在闭包
    中。为了解决这个问题,Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。最新版 make_averager 的正确实现如示例6所示。

    def make_averager():
        count = 0
        total = 0
        def averager(new_value):
            nonlocal count, total
            count += 1
            total += new_value
            return total / count
        return averager




























  • 相关阅读:
    03 python基础——logging模块
    六、Celery任务执行超时的设置
    python 单例模式线程安全的示例
    Linux samba磁盘的挂载
    Linux 添加新硬盘无法识别[无需重启]的解决方法
    Windows注册表关闭系统服务
    五、Celery 高级用法【20200912】
    四、Celery 路由学习篇
    三、Celery Linux操作系统后台启动的服务制作
    二、Python Celery 4.4.7 与 Django 2.X的集成【定时任务的调用】
  • 原文地址:https://www.cnblogs.com/chenxuming/p/9710785.html
Copyright © 2011-2022 走看看