Python中的闭包global关键字nonlocal关键字和装饰器

Posted 魏晓蕾

tags:

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

1、闭包

闭包:在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用,这样就构成了一个闭包。

# 闭包函数的实例
# outer是外部函数 a和b都是外函数的临时变量
def outer(a):
    b = 10
    # inner是内函数
    def inner():
        # 在内函数中 用到了外函数的临时变量
        print(a+b)
    # 外函数的返回值是内函数的引用
    return inner
 
if __name__ == '__main__':
    # 在这里我们调用外函数传入参数5
    # 此时外函数两个临时变量 a是5 b是10 ,并创建了内函数,然后把内函数的引用返回存给了demo
    # 外函数结束的时候发现内部函数将会用到自己的临时变量,这两个临时变量就不会释放,会绑定给这个内部函数
    demo = outer(5)
    # 我们调用内部函数,看一看内部函数是不是能使用外部函数的临时变量
    # demo存了外函数的返回值,也就是inner函数的引用,这里相当于执行inner函数
    demo() 					# 15

    demo2 = outer(7)
    demo2()					# 17

外函数把临时变量绑定给内函数:
按照我们正常的认知,一个函数结束的时候,会把自己的临时变量都释放还给内存,之后变量都不存在了。一般情况下,确实是这样的。但是闭包是一个特别的情况。外部函数发现,自己的临时变量会在将来的内部函数中用到,自己在结束的时候,返回内函数的同时,会把外函数的临时变量送给内函数绑定在一起。所以外函数已经结束了,调用内函数的时候仍然能够使用外函数的临时变量。
在我编写的实例中,我两次调用外部函数outer,分别传入的值是5和7。内部函数只定义了一次,我们发现调用的时候,内部函数是能识别外函数的临时变量是不一样的。python中一切都是对象,虽然函数我们只定义了一次,但是外函数在运行的时候,实际上是按照里面代码执行的,外函数里创建了一个函数,我们每次调用外函数,它都创建一个内函数,虽然代码一样,但是却创建了不同的对象,并且把每次传入的临时变量数值绑定给内函数,再把内函数引用返回。虽然内函数代码是一样的,但其实,我们每次调用外函数,都返回不同的实例对象的引用,他们的功能是一样的,但是它们实际上不是同一个函数对象。

闭包中内函数修改外函数局部变量:
方式一:声明global变量
方式二:声明nonlocal变量
方式三:把闭包变量改成可变类型数据进行修改,比如列表

2、global 关键字

global 关键字修饰的变量是全局变量,对该变量进行修改就是修改全局变量,可以使用在任何地方,包括上层函数、嵌套函数、main函数。

3、nonlocal 关键字

nonlocal关键字修饰变量后标识该变量是上一级函数中的局部变量,nonlocal关键字只能用于嵌套函数中,并且外层函数中定义了相应的局部变量,否则会发生错误。

# 修改闭包变量的实例
# outer是外部函数 a和b都是外函数的临时变量
def outer( a ):
    b = 10  					# a和b都是闭包变量
    c = [a] 					# 这里对应修改闭包变量的方法2
    # inner是内函数
    def inner():
        # 内函数中想修改闭包变量
        # 方法1:nonlocal关键字声明
        nonlocal  b
        b+=1
        # 方法二:把闭包变量修改成可变数据类型,比如列表
        c[0] += 1
        print(c[0])
        print(b)
    # 外函数的返回值是内函数的引用
    return inner

if __name__ == '__main__':
    demo = outer(5)
    demo() 						# 6  11


使用闭包的过程中,一旦外函数被调用一次返回了内函数的引用,虽然每次调用内函数,是开启一个函数执行过后消亡,但是闭包变量实际上只有一份,每次开启内函数都在使用同一份闭包变量。

def outer(x):
    def inner(y):
        nonlocal x
        x+=y
        return x
    return inner 

a = outer(10)
print(a(1)) 					# 11
print(a(3)) 					# 14

两次分别打印出11和14,由此可见,每次调用inner的时候,使用的闭包变量x实际上是同一个。

4、global 和 nonlocal 关键字对比

global 关键字和 nonlocal 关键字不能同时修饰同一个变量,将 global 关键字换成 nonlocal 关键字会得到不同的结果。

  • global 关键字和 nonlocal 关键字修饰不同的变量:

  • global 关键字和 nonlocal 关键字修饰同一个变量:

  • 将 global 关键字换成 nonlocal 关键字,输出不同的效果:

5、改变外函数的参数值

外函数参数如果在内函数中不改变值,则可以在内函数中引用,比如print打印,如果要在内函数中改变外函数的参数,则需要在内函数中声明该参数为nonlocal,而如果声明为global会报错。

6、装饰器

由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。

>>> def now():
...     print('2015-3-25')
...
>>> f = now
>>> f()
2015-3-25

函数对象有一个__name__属性,可以拿到函数的名字:

>>> now.__name__
'now'
>>> f.__name__
'now'

现在,假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。

本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:

def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:

@log
def now():
    print('2015-3-25')

调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:

>>> now()
call now():
2015-3-25

把@log放到now()函数的定义处,相当于执行了语句:

now = log(now)

由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。

wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。

如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:

def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

这个3层嵌套的decorator用法如下:

@log('execute')
def now():
    print('2015-3-25')

执行结果如下:

>>> now()
execute now():
2015-3-25

和两层嵌套的decorator相比,3层嵌套的效果是这样的:

>>> now = log('execute')(now)

我们来剖析上面的语句,首先执行log(‘execute’),返回的是decorator函数,再调用返回的函数,参数是now函数,返回值最终是wrapper函数。

以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的’now’变成了’wrapper’:

>>> now.__name__
'wrapper'

因为返回的那个wrapper()函数名字就是’wrapper’,所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

不需要编写wrapper.__ name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的decorator的写法如下:

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

或者针对带参数的decorator:

import functools

def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

import functools是导入functools模块。模块的概念稍候讲解。现在,只需记住在定义wrapper()的前面加上@functools.wraps(func)即可。
练习
请设计一个decorator,它可作用于任何函数上,并打印该函数的执行时间:

import functools
import time

def metric(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kw):
        start = time.time()
        result = fn(*args, **kw)
        print('%s executed in %s ms' % (fn.__name__, str(time.time() - start)))
        return result
    return wrapper

@metric
def fast(x, y):
    time.sleep(10)
    return x + y

@metric
def slow(x, y, z):
    time.sleep(10)
    return x * y * z

f = fast(11, 22)
s = slow(11, 22, 33)
if f != 33:
    print('加法测试失败!')
elif s != 7986:
    print('乘法测试失败!')
print('f=',f)
print('s=',s)
fast executed in 10.00390100479126 ms
slow executed in 10.014141082763672 ms
f= 33
s= 7986

以上是关于Python中的闭包global关键字nonlocal关键字和装饰器的主要内容,如果未能解决你的问题,请参考以下文章

python中的关键字global和nonlocal

Py3 nonlocal和global的不同

python中global 和 nonlocal 的作用域

Python 2.x 中的 nonlocal 关键字

Python 为什么引入这两个关键词

Python 为什么引入这两个关键词