python 面试

Python闭包(Closure)的理解及应用

技术分享

systemime
2021-05-25
10 min

对python闭包进行理解说明

对于初学者来说,closure 可能是个比较难懂的概念。来看看 wiki 上对于 closure 的说法:

a closure (also lexical closure or function closure) is a technique for implementing lexically scoped name binding in a language with first-class functions_

嗯… 应该没有看懂吧?

其实文中所提的 binding 其实白话的来说,就是变量在一个函数中是如何被决定它的值会是多少 (lookup)。

而 lexical binding ,根据 EmacsWiki 上的说明,就是每个 scope (function, class, …etc) 会有各自的一张 variable lookup table 。

# LEGB

根据 Python.org 的 tutorial 中的说明,当一个变量被使用时,会遵循 LEGB 的规则,也就是 Local、Enclosing、Global 与 Builtins

让我以下面这段代码为例吧:

    glob = 3 
    def func(x):  
        y = x + glob  
        def inner():  
            return y + 1  
        return inner, abs

Local,顾名思义,就是在 local variables 里查找。以上面的例子来说, y 就是 func的 local variable ,因为 y 是在 func 里才被定义的。

Enclosing,也就是 enclosing scope (别急,等会解释)

Global,也就是 global variable ,在上面的例子里, func 里用到的 glob 就会是定义在 func 外面的 glob

Builtins,也就是从 builtins 模组里去找,上面的例子里就是最后用到的 abs

当以上都找不到这个你要的变数时,就会引发 NameError

# Function Scope

上面的 LGEB 有提到 enclosing scope。在 Python 里创造一个 scope 的最简单的方式是 function 。顺道一提,在 Python 里 for 是不会创造一个 scope 的喔!譬如你可以试试下面这串程式码:

    for i in range(10):  
        pass  
    print(i)  
    # 应该会看到 9

p.s 有机会再来聊聊 Python 的 lazy binding 好了,有机会的话….?

言归正传。也就是说,在 Python 中当你定义一个 function 时,你就创造了一个 scope ,这个 scope 会影响到你这个 function 里所有 local 与 non-local 变数会如何被参照。

(可以参考: Python 命名空间和作用域窥探)

local 变数我想大家应该不陌生,但这 non-local 又是啥鬼?

这主要是因应 nested scope 而衍生的定义。由于在 Python 里,所有东西都是 object ,而 object 是 first-class citizen (定义看这边),所以 function 也是 first-class citizen 。具体来说,由于这样的设计,你可以在 function 里定义 function 并回传 function 。又因为每定义一个 function 就会产生一个 scope ,所以当你在 function 里又定义 function 时,一个 nested scope 就会被产生 (不负责任白话翻译: 包在 scope 里的 scope)

上述 Python.org 的 tutorial 里就有提到,当走到 LEGB 的 E 时,Python 会从最近的 enclosing scope 向外找起,那这些 enclosing scopes 里的所有变数,就是所谓的 non-local variable。

举例说明好了

    def outer(a):  
        b = a  
        def inner():  
            c = 3  
            def inner_inner(b):  
                r = b+c  
                return b+c  
            return inner_inner  
        return inner  
    foo = outer(10)  
    bar = foo()  
    bar(1) # 4

outer 来说, b 是它的 local variable;对 inner 来说, c 是它的 local variable。

另一方面虽然没用到,但是 b 是它的 non-local variable (因为离它最近的 scope 是 outer 所创造出来的 scope,而 b 是在这个 scope 里的);对 inner_inner 来说, r 是它的 local variable ,值被指定为 b+c ,而这时的 b 并不是在 outer 的 scope 里被创造的 b ,而是经由参数传递进来的,所以也是 local variable 。

反观 c ,则是 inner 的 scope 里的 c ,对 inner_inner 来说,是属于 non-local variable。

# Under the Hood: closure

在科技里,我总是相信不存在魔法。所以到底 Python 的开发者是如何实现 function closure 的?

老实说我 C 很烂,所以我找不出来 CPython 是怎么实作 closure 的,但至少,我可以从 Python 官方的 language reference 找答案

其实也不是什么稀奇的事,就是 __closure__ 这个属性 (attribute) 。根据 Python 的 Data Model 的定义, __closure__ 会是一个唯读属性;资料型态是 tuple ,所以是 immutable 的。

那知道这个能干嘛?又是 read-only 又是 immutable ,好像也不知道能对它做什么。

说实在的,我也不知道能干嘛,但 language reference 里的一句话引起了我的注意:

_None_ or a tuple of cells that contain bindings for the function’s free variables. See below for information on the _cell_contents_ attribute.

那至少,我们可以用 __closure__ 检视我们对于 Python closure 的理解吧?动手试试吧!

  1. 有 non-local variable 就会有 __closure__ ?

错!要有用到才会形成 __closure__ ,否则就是 None

    def foo():  
        a = 3  
        def bar():  
            print("bar")  
        return bar  
    bar = foo()  
    bar.__closure__ is None  
    # >>> True

这个例子有趣的地方是,你可以发现当一个 function 的内容并没有用到任何 free variable 时,这时 __closure__ 会是 None 。以上面的例子来看,虽然对 bar 来说,有个 a 这个 non-local variable ,但由于 bar 没有用到它,也因此没有任何 free variable,所以这时 bar.__closure__ 也就还是 None

  1. 没有 free variable 就不会有 __closure__ ?

错!如果 inner scope 有用到 free variable ,就会被包含到 outer scope 里。

    def foo():  
        a = 3  
        def bar():  
            def hell():  
                return a  
            return hell  
        return bar  
    bar = foo()  
    bar.__closure__ is None  
    # False  
    bar.__closure__  
    # (<cell at 0x109d54408: int object at 0x10787eaf0>,)  
    bar().__closure__ == bar.__closure__  
    # True   
    bar.__closure__[0].cell_contents  
    # 3

在这个例子里,虽然 bar 没有用到任何 free variable ,但是 hell 透过 nested scope 取得了 foo 里的 a ,间接影响了 bar.__closure__

  1. Bonus Question

以下代码有错误吗?

    def foo():  
        def bar():  
            return bar # Error?  
        return bar  
    bar = foo() # Error?

会有 error 吗?

如果没有的话,想想为什么吧 : ) (Hint: LEGB)

# 学 closure 要干嘛?

如果你的背景是 JavaScript ,我想这个问题应该是再明显不过,当然非常实用!举例来说,在 JavaScript 里不乏看到这样的代码:

    (function(msg){  
      var x = 3;  
      function inner(){  
          // do things with x and msg  
      }  
      return inner;  
    })('my awesome string');

也就是利用匿名函数的 scope 进行 variable 的隔离 (例如上面的 x)。

很不幸的,Python 的 lambda 只能有一句 statement ,很难做到等价的 JavaScript code ,只能用 def (但这样就不匿名了 Q_Q)。

但除此之外,常见的就会是可带参数的 decorator 。假设你现在必须写个 function 的 decorator ,他会让函数在回传 None 时,改回传另一个你指定的值,那简单的实作可能会长这样:

    def return_default(value):  
        def deco(func):  
            def wrapped(*args, **kwargs):  
                ret = func(*args, **kwargs)  
                if ret is None:  
                    ret = value  
                return ret  
            return wrapped  
        return deco

接着你可以这样使用 return_default :

    @return_default(10)  
    def at_least_10(x):  
        if x >= 10:  
            return x  
    @return_default("python")  
    def greeting(msg):  
        return msg  
    x = at_least_10(3) # 10  
    msg = greeting(None) # "python"

简单的说, return_default 这个 decorator 利用了 closure ,在自己的 scope 里定义了一个 inner scope 把 value 放在里面的 deco 的 closure 里,才能做到这种夹带参数的 decorator 。

如果我们用刚刚学过的 __closure__ 检视,就能发现:

    return_default(10).__closure__  
    # (<cell at 0x109d54528: int object at 0x10787ebd0>,)  
    return_default(10).__closure__[0].cell_contents  
    # 10  
    return_default("hello").__closure__[0].cell_contents  
    # 'hello'

如果这样还说服不了你,那让我用几个有名的 open source project 的 code 当例子吧:

好啦,Python 的 function closure 就简单介绍到这里啦。

Happy Python Programming!

# References

改编自: dboyliao@Medium https://medium.com/@chenomg/python%E9%97%AD%E5%8C%85-closure-%E7%9A%84%E7%90%86%E8%A7%A3%E5%8F%8A%E5%BA%94%E7%94%A8-f2ced3011b26

上次编辑于: 5/25/2021, 6:54:57 AM