python中的闭包

Posted hi_mxd

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python中的闭包相关的知识,希望对你有一定的参考价值。

 

在理解闭包之前,首先要明确什么是嵌套函数(nested function)以及非局部变量(nonlocal variable);

嵌套函数:一个函数定义在另一个函数内部,称为嵌套函数;

 

1. 基础知识

Python中的变量范围(scope):

变量的作用范围:变量的作用范围指的在什么范围内变量可以被访问;

一个变量只能在它的作用范围之内被获取;

变量的作用范围通常由它在代码中被赋值的位置决定,一般来说,分为三种不同的范围:

1. 全局范围:在所有函数的外面被定义,可以被程序中的所有函数进行访问;

2.局部范围:如果一个变量在函数内部定义,那么对这个函数来说它就是局部的。局部变量只能在定义它的函数内部被访问;

3.非局部范围:当一个变量在一个封闭函数中被赋值时,对于嵌套函数来说,他就是非局部的。非局部变量可以由定义它的函数以及嵌套函数访问。

 

如果在函数内部重新分配一个全局变量,那么将会创建一个具有相同名称的新的局部变量,并且覆盖掉之前的全局变量。在函数内部对这个局部变量的任何更改,都不会影响全局变量。所以如果想在函数内部改变一个全局值,必须使用Python中的global关键字。

举例:

 

 1 # Listing 1
 2 x = 1 # x is a global variable  
 3 y = 5 # y is a global variable 
 4 def f():
 5     global y 
 6     x = 2   # x is a local variable
 7     y += 1  # Reassigning the global variable y
 8     z = 10   # z is a local variable
 9     print("Local variable x =", x)
10     print("Global variable y =", y)
11     print("Local variable z =", z)
12 f()
13 print("Global variable  x =", x)
14 print("Global variable y =", y)

输出:

1 Local variable x = 2
2 Global variable y = 6
3 Local variable z = 10
4 Global variable  x = 1
5 Global variable y = 6

可以看出,函数f()外面的x不受函数内部的变量值变化影响,而y由于被声明为全局变量,因此最终值改变。

 

在python中,任何东西都是一个对象,变量是对这些对象的引用;

当你给一个函数传递一个变量的时候,Python同时会传递该变量所引用对象的引用副本;

也就是,它不会把对象或者原始的引用传递给函数中,而是copy了一份传给函数;

因此,函数作为参数接收到的原始引用和复制引用都指向同一个对象。

现在,如果我们传递一个不可变的全局对象(如整数或字符串),函数不能使用其参数修改它。但是,如果对象是可变的(如列表),函数可以修改它。下面是一个例子:

 

1 Listing 2
2 a = [1, 2, 3]
3 b = 5
4 def func(x, y):
5     x.append(4)
6     y = y + 1
7 func(a, b)
8 print("a=", a)  #  Output is a=[1, 2, 3, 4]
9 print("b=", b)  #  Output is b=5

当我们传递a和b两个变量给函数的时候,a是一个可变的变量,b是一个整数型变量,是不可变的;

fun收到一个a的拷贝,作为x,b的拷贝作为y。

向x添加一个新元素将改变a所引用的原始对象。但是在func中给y加1并不影响b所指的对象。它只创建一个包含6的新整数对象(图1)

 

 

 

 二、嵌套函数(内部函数)

定义:是个定义在其他函数(outer 函数)内部的函数;

outer函数中定义的局部变量对于嵌套函数来说就是非局部变量(可以访问,但不可以修改)

类比于,在函数中修改全局变量,在嵌套函数中修改非局部变量的话,将会创建一个新的变量;

因此,如果你想要修改非局部变量的值的话,需要使用nonlocal方法;

简而言之:

在外部函数中声明的函数

从外部函数视角看:是局部变量;

从嵌套函数视角看:是非局部变量,不可修改,但是可以访问;

 1 # Listing 3
 2 def f(x):
 3     y = 5
 4     z = 10
 5     t = 10
 6     def g():
 7         nonlocal y
 8         y += 1
 9         z = 20
10         print("Nonlocal variable xg =", x) #5
11         print("Local variable zg =", z)    #20
12     print("Local variable t =", t)    #10
13     g()
14     print("Nonlocal variable x =", x) #5
15     print("Nonlocal variable y =", y) #6
16     print("Local variable z =", z)   #10
17 f(5)
18 # This does not work:
19 # g()

输出:

1 Local variable t = 10
2 Nonlocal variable x1 = 5
3 Local variable z1 = 20
4 Nonlocal variable x = 5
5 Nonlocal variable y = 6
6 Local variable z = 10

对于嵌套函数g()来说,x和y都是非局部变量,可以被g()访问;

在g()中创建的新变量z是不会影响到f()中的z值;

然而,使用nonlocal关键字之后,y可以在嵌套函数g()中被修改。

变量t对于g来说是根本就没有访问到,因此它不是g()的非局部变量;

 

变量y和z都属于f()的局部变量,在f()的外部不可见;同时g()在函数的外面也是不可见的;

 

如果我们找到一个方法可以在f()外部调用g(),我们将会面临第二个问题——当我们退出外部函数的时候,它的局部变量(也就是g()的非局部变量不能够再被获取)。闭包——产生;

闭包使在外部函数之外调用内部函数时访问其非局部变量称为可能。

 

三、闭包:

首先,我们要找到一个方法来实现在外部函数调用内部函数;

请记住,一个函数是可以返回一个值的,Listing 4中,x和y是f()的局部变量:

1 # Listing 4
2 def f():
3     x = 5
4     y = 10
5     return x
6 h=f()

在运行完h=f()之后,所有f()的局部变量都将消失,也就是x和y将无法被访问;

但是我们仍然有x的值,因为它被返回同时存储在了h中。

因此,我们可以创造一个外部函数来返回嵌入函数;

这在Python中是有可能的,因为Python是可以把函数分配给变量的,由于变量可以作为函数的参数进行传入或者返回,所以函数也可以作为函数的参数进行输入或者是返回。

1 # Listing 5
2 def f(x):
3     def g(y):
4         return y
5     return g
6 a = 5
7 b = 1
8 h=f(a)
9 h(b)  # Output is 1

现在f(x)返回函数g,因此我们可以写做h=f(a),把g分配给h,现在h就可以看作是g,接收g的参数,因此,h(b)就等同于g(b)

 

函数的__name__属性存储了定义该函数时使用的名称。

h.__name__   

输出:

\'g\'

 

也就是说,h现在是函数g的引用;

 

需要注意的是f(x)返回的是一个函数g,而不是一个特定的值;

比如,如果我们写成:

# Listing 6
def f(x):
    def g(y):
        return y
    return g(y)
a = 5
b = 1
h=f(a) 
# This does not work:
# h(b)

同时,也会出现y没有定义的错误;

 

我们不需要在h中存储f(a),实际上我们可以直接调用f(a)(b)

 

# Listing 7
def f(x):
    def g(y):
        return y
    return g
a = 5
b = 1
f(a)(b)  # Output is 1

注意:f(a,b)和f(a)(b)的区分;f(a,b)是一个含有两个参数的函数;而f(a)(b)则是嵌套函数;每个函数都接收一个参数;

如下图所示,f(a)=g,因此f(a)(b) = g(b)

 

 

有了上述基础,我们就很容易将其拓展到多个嵌套函数中:

 1 # Listing 8
 2 def f(x):
 3     def g(y):
 4         def h(z):
 5             return z
 6         return h
 7     return g
 8 a = 5
 9 b = 2
10 c = 1
11 f(a)(b)(c)  # Output is 1

类似于f(g(h(z)))

那么,如果有非局部变量的话,结果又是怎么样的呢?

 1 # Listing 9
 2 def f(x):
 3     z = 2
 4     def g(y):
 5         return z*x + y
 6     return g
 7 a = 5
 8 b = 1
 9 h = f(a)
10 h(b)  # Output is 11

在运行完f(x)之后,变量x和z应该是不可访问的,但是为什么g(y)仍然可以访问他们?这是因为嵌套函数g(y)现在是一个闭包(closure)。

 

闭包:闭包是一个嵌套函数,可以访问外部函数中的非局部变量;因此,对于在enclosing范围内的非局部变量,即使不在内存中,也仍然可以被记住;

 

对于嵌套函数g()来说,如果只用到了内部局部变量y,则变量y称为bound variable;g()称为close term;

对嵌套函数中的非局部变量,称为free variable,函数g()称为open term;

 

 闭包的名字来自于这样的事实:它可以捕获其绑定的自由(非局部)变量,是一个open term关闭的结果;

 

也就是说,闭包,就是给free变量(非局部变量)绑定变量返回的函数;一旦参数被绑定了,最后可以调用返回的函数,无论外层的函数是否被销毁;

 

 

 Python中可以追踪每个函数中的free variables,使用.__code__.co_freevars可以看到被嵌套函数捕获的自由变量,对于Listing 9 来说:

h.__code__.co_freevars

输出为:

(\'x\', \'z\')

同样也可以使用closure属性来获得这些自由变量的值

print(h.__code__.co_freevars[0], "=",
      h.__closure__[0].cell_contents) 
print(h.__code__.co_freevars[1], "=",
      h.__closure__[1].cell_contents)

输出为:

 

x = 5
z = 2

 

重点:对闭包来说,嵌套函数应该可以访问外部函数的非局部变量;

当在嵌套函数内部没有可访问的自由变量时,此时,嵌套函数为闭项(closed term)。

下面的代码就不能被认为是闭包。这是由于非局部变量x和z没有在g(y)中被访问,也就不需要g(y)来捕获他们。

 1 # Listing 10
 2 def f(x):
 3     z = 2
 4     def g(y):
 5         return y
 6     return g
 7 a = 5
 8 b = 1
 9 h = f(a)
10 h(b)  # Output is 1

我们可以通过.__code__.co_freevars来进行检查:

h.__code__.co_freevars

输出为:

  () 

此外,h.__closure返回 None;同样也表示了h()不再是一个闭包;

但是如果在嵌套函数中定义了非局部变量,它仍然是闭包:

 1 # Listing 11
 2 def f(x):
 3     z = 2
 4     t = 3
 5     def g(y):
 6         nonlocal t
 7         return y
 8     return g
 9 a = 5
10 b = 1
11 h = f(a)
12 h(b)  
13 h.__code__.co_freevars  # Output is (\'t\',)

另一个例子:

1 # Listing 12
2 def f(x):
3     def g(y = x):
4         return y
5     return g
6 a = 5
7 b = 1
8 h = f(a)
9 h()  # Output is 5

这里g(y)不是一个闭包,因为x的值只是用于初始化y,而g不需要捕获x。

 

如果有多重嵌套函数,每个闭包都能够捕捉所有高层的非局部变量;

举例来说:

 1 # Listing 13
 2 def f(x):
 3     def g(y):
 4         def h(z):
 5             return x * y * z
 6         return h
 7     return g
 8 a = 5
 9 b = 2
10 c = 1
11 f(a)(b)(c)  # Output is 10

h(z)可以捕获到f和g非局部变量,因此:

# f(a)(b) refers to h
f(a)(b).__code__.co_freevars

的输出为:

(\'x\', \'y\')

此外,g(y)也是个闭包,因为可以捕捉到x为非局部变量,这一点很容易证实:

f(a).__code__.co_freevars  # Output is (\'x\',)

 

但是下列代码中g(y)就不是一个闭包了,

 1 # Listing 14
 2 def f(x):
 3     def g(y):
 4         def h(z):
 5             return y * z
 6         return h
 7     return g
 8 a = 5
 9 b = 2
10 c = 1
11 f(a).__code__.co_freevars  # Output is ()

这是因为,h(z)不需要获取x的值;因此g(y)同样也就不需要获取x的值,也就不是一个闭包了;

 

也许你可能注意到了,在调用闭包的时候,我们不需要使用内部函数的名字,只需要使用外部函数的名字即可;

基于此,在一些情况下(比如我们只有一条表达式),可以使用lambda函数来替换内部函数;

举例来说:

1 # Listing 15
2 def f(x):
3     z = 2
4     return lambda y: z*x+y
5 a = 5
6 b = 1
7 f(a)(b)  # Output is 11

现在,我想总结一下到目前为止我们学到的闭包知识。要定义闭包,我们需要一个内部函数:

 

1-它应该由外部函数返回。
2.它应该包含外部函数的一些非局部变量。这可以通过访问这些变量,或将它们定义为非局部变量,或使用需要捕获它们的嵌套闭包来实现。

 

定义闭包之后,要初始化它,必须调用外部函数来返回闭包

 

在函数式编程中,闭包可以将数据绑定到函数,而不必实际将它们作为参数传递。这类似于面向对象编程中的类。在清单16中,我们比较了这些范例。我们首先创建一个类来计算一个数的n次方根。

 1 # Listing 16
 2 class NthRoot:
 3     def __init__(self, n=2):
 4         self.n = n
 5     def set_root(n):
 6         self.n = n
 7     def calc(self, x):
 8         return x ** (1/self.n)
 9     
10 thirdRoot = NthRoot(3)
11 print(thirdRoot.calc(27))  # Output is 3
12 def nth_root(n=2):
13     def calc(x):
14         return x ** (1/n)
15     return calc
16 third_root = nth_root(3)
17 print(third_root(27))  # Output is 3

外部函数扮演了一个构造器的作用,首先初始化了非局部变量,这些非局部变量将在嵌套函数中使用;

然后这里也有一些不同:

NthRoot可以通过thirdRoot对象实现更多的方法调用;而nth_root只是函数本身。

所以相比于类来说,这些方法是更加受限的。

现在既然我们已经熟悉了这些闭包,接下来,就可以看一下他们有哪些应用;

 

compostion

如果我们有两个函数f和g,我们可以使用下面的方法使得f的输出成为g的输入。

1 # Listing 17
2 def compose(g, f):
3     def h(*args, **kwargs):
4         return g(f(*args, **kwargs))
5     return h

h是一个闭包,因为它捕获了非局部变量f和g;这些非局部变量是函数自身。

这个闭包的返回值为f和g的组成,也就是g(f(*args, **kwargs))。我们使用*args和**kwargs可以给h传递多个参数;

具体来说,我们希望两个函数英寸转换为英尺;英尺转换为米;使用compose()函数可以直接将英寸转换为米;

 

# Listing 18
inch_to_foot= lambda x: x/12
foot_meter= lambda x: x * 0.3048
inch_to_meter = compose(foot_meter, inch_to_foot)
inch_to_meter(12)   # Output 0.3048

 

四、装饰与装饰器

 

在讨论装饰器之前,我需要介绍一下Python中的函数。在Python中定义一个函数时,该函数的名称只是对函数体的引用(函数定义)。因此,通过给它赋一个新值,可以强制它引用另一个函数定义。清单22给出了一个简单的示例:

 

 1 # Listing 22
 2 def f():
 3     return("f definition")
 4 def g():
 5     return("g definition")
 6 print("f is referring to ", f())
 7 print("g is referring to ", g())
 8 print("Swapping f and g")
 9 temp = f
10 f = g
11 g = temp
12 print("f is referring to ", f())
13 print("g is referring to ", g())

输出:

1 f is referring to  f definition
2 g is referring to  g definition
3 Swapping f and g
4 f is referring to  g definition
5 g is referring to  f definition

由此可见,我们可以像交换变量一样交换函数;

到目前为止,我们已经通过将外部函数的结果赋值给一个新变量来创建了一个闭包:

h=f(a)

现在假设我们有一个名为deco(f)的外部函数,它在清单23中定义:

1 # Listing 23
2 def deco(f):
3     def g(*args, **kwargs):
4         return f(*args, **kwargs)
5     return g
6 def func(x):
7      return 2*x
8 func = deco(func)
9 func(2)  # Output is 4

外部函数deco(f),参数是函数(f);

内部函数g定义为一个闭包;

定义另一个函数func,并且作用deco方法给func函数;

为了初始化闭包,我们把deco的结果分配给了func;

也就是说,deco的参数为func,同时又把闭包分配给了func;

在这种情况下,我们说func被deco装饰了;

deco是func的一个装饰器;

 

 

 在把装饰器的结果分配给func以后,func代表g的引用,因此调用func(a)就等于调用g(a)。

测试:

func.__name__ #output \'g\'

事实上,变量func仅仅是函数的引用;

刚开始的时候,它表示func(x)定义,但是在经过装饰以后,它表示的是g()

但是对原始函数func(x)来说发生了什么?它是否改变了?

请记住,装饰器接收func 作为它的参数,因此它有一个func引用的拷贝作为它的局部变量f。

如果原始的引用改变了,也不会影响这个局部变量;

因此,在g的内部,f仍然表示的是func(x)定义;

 

 

 

 

 

总结:

经过装饰器以后变量func表示的是g闭包;

在g的内部,f表示的是func(x),被装饰的函数;

我们不能直接在g的外面调用func(x);

相反的,我们首先调用func来调用g,之后在g的内部,我们调用f来调用原始的func(x)。因此我们通过使用闭包g来调用原始函数func(x),

 

现在使用这个闭包,我们可以在调用func(x)前后添加更多的代码;

举个简单的例子;加入我们正在给清单23进行debug,我们想要知道func(x)什么时候被调用的,我们可以简单的在func(x)中内嵌一个print函数:

def func(x):
    print("func is called") 
    return 2*x

 

但是它改变了函数,我们必须记住以后把它删除。更好的解决方案是定义一个装饰器来包装函数,并将print语句添加到闭包中。

输出:

Calling  func
4

 

五、装饰器的应用:

缓存:

缓存是一种用于加快程序运行速度的编程技术。它来源于拉丁语“备忘录”,意思是“被记住的”。顾名思义,它基于对代价高的函数调用结果的记忆或缓存。

如果输入相同的输入或具有相同参数,将使用前面缓存的结果以避免不必要的计算。

在Python中,我们可以使用闭包和装饰器自动缓存函数。

清单25显示了一个计算斐波那契数列的函数。Fibonacci数是递归定义的。每个数字都是前面两个数字的和,从0和1开始:

 1 # Listing 25
 2 def fib(n):
 3     if n == 0:
 4         return 0
 5     elif n == 1:
 6         return 1
 7     else:
 8         return fib(n-1) + fib(n-2)
 9 for i in range(6):
10     print(fib(i), end=" ")
11 # Output
12 # 0 1 1 2 3 5

 

现在我们定义一个新函数,它可以缓存另一个函数:

1 # Listing 26
2 def memoize(f):
3     memo = {}
4     def memoized_func(n):
5         if n not in memo:            
6             memo[n] = f(n)
7         return memo[n]
8     return memoized_func

 

fib = memoize(fib)
fib(30) # Output is 832040

fib被memoize()进行了装饰;此时,fib指代的是memoized_func,因此当我们调用fib(30)的时候,实际上,我们调用的是memoized_func(30)。

装饰器接收到原始的fib作为它的参数f,因此,定义在memoized_func函数中的f表示的是fib(n);

如果n不在memo字典中,它首先调用f(n),也就是原始的fib(n),之后把结果存储在memo字典中;最后返回最终的结果;

原文链接:https://towardsdatascience.com/closures-and-decorators-in-python-2551abbc6eb6

 

以上是关于python中的闭包的主要内容,如果未能解决你的问题,请参考以下文章

Spark闭包与序列化

python中的闭包

python闲谈--闭包

python中的闭包

scala编程——函数和闭包

python中的闭包,迭代器.