Python中的装饰器

最近学习了Fluentpython中的装饰器的相关知识,在此分享一下学习过程中的心得。(以下代码示例均为Fluentpython中的代码)

1.Python中变量的作用域以及作用时间

Python中变量的作用域和C一样分为局部变量和全局变量,其中函数中定义的变量为一般局部变量,函数之外定义的一般为全局变量。

 

def f1(a):
    print(a)
    print(b)

f1(3)

# >>> NameError: name 'b' is not defined,因为b是局部变量并且没有定义

执行上面这段代码的时候,明显f1()中的b为局部变量,但是没有定义,所以执行的时候必然会报错

b = 6
f1(3)
# >>> 3
# >>> 6

如果在函数之前先定义了b,如上所示,则此时明显可以运行出正确的结果

但是如果此时在f1函数中对b进行了重新的赋值,运行结果如下所示:

def f1(a):
    print(a)
    print(b)
    b = 9
b = 6
f1(3)  # >>> UnboundLocalError: local variable 'b' referenced before assignment

因为此时的b是不可变对象,f1中的b=9的本质是重新创建了一个对象b并且将它进行赋值,此时的b即为局部变量,并且绑定值是在print(b)之后进行的,而Python解释器会尝试从本地环境获取b,但是当尝试获取本地局部变量b的时候发现b没有绑定值,所以就会出现上述的错误

def f1_new(a):
    b = 9
    print(a)
    print(b)

b = 6
f1_new(3) # >>> 3
          # >>> 9
b  # >>> 6

当将代码改成上述这样之后,可以看到此时可以输出a,b的值,但是外部全局变量b的值并没有被改变,本质就是因为b是不可变的,f函数中的b=6本质是重新创建了一个对象,为了验证这一点可以将b变成可以对象,如列表:

def f1_new_1(a):
    print(a)
    print(b)
    b.append(4)

b = [1,2,3]
f1_new_1(3) # >>> 3 [1,2,3]
b  # >>> [1,2,3,4]

此时就不会存在任何问题,因为b是可变对象

对于函数f1如果想将b变成全局变量,则在Python3中需要使用global全局声明,此时即使不可变对象b在函数中被改变了,其仍然是全局变量(可以想象本质应该是利用C中的指针实现的,实现的原理也很简单)

def f1_new_3(a):
    global b  # 将b声明为全局变量
    print(a)
    print(b)
    b = 9


f1_new_3(3)  # >>> 3 9
b  # >>> 9

Python中除了局部变量和全局变量之外,还有自由变量,所谓自由变量指的是存在函数嵌套的时候,被嵌套函数中未绑定的变量

def f2():
    series = []
    total = 0
    count = 0

    def f3():
        nonlocal count
        series.append(1)
        total += 1
        count += 1
    return f3

上述中f3中的series就是自由变量(类似于上述中的局部变量),虽然在f3中的series被改变了,但是由于其是可变对象,所以仍然是自由变量,但是total是不可变对象,所以在f3中被改变之后就不是自由变量了,而是局部变量,此时如果还想其为自由变量,与上述f1中的声明相似,只不过此时不是使用global声明,而是使用nonlocal声明。

综上可以看出自由变量可以看做是局部变量,只不过是套在函数中的函数中的局部变量,而可以将外层函数中对应的变量看成全局变量。

2.闭包

有了变量作用域的知识之后,就可以知道闭包的作用了,闭包本质就是一个函数,其中存放的是嵌套函数中的自由变量以及其绑定的值,为什么要存在闭包呢,因为外层函数在返回之后其中定义的变量就失去作用了,但此时内层函数一般还想继续使用外层函数中定义的变量,那么为什么内存函数还会存在呢?因为Python中的装饰器一般都会返回内层函数(外层函数的返回结果),所以这也是为什么说闭包是装饰器基础的原因。简而言之,Python中的装饰器一般会返回内层函数,但是内层函数会使用外层函数中的变量,但是完成函数在执行完成之后其中定义的变量就失效了,为了解决这个问题,Python中就有了闭包,闭包就是专门存放内层函数中需要使用到的外层函数的变量(内层函数中称为自由变量)以及其绑定的值的!可以使用函数的属性查看闭包中保存的值

f = f2()  
f.__code__.co_freevars  # >>> ('count', 'series')

f.__closure__[0].cell_contents  # >>> 0
f.__closure__[1].cell_contents  # >>> []
f()  # >>> UnboundLocalError: local variable 'total' referenced before assignment

可以看出只有count和series是自由变量,并且运行f会出错,因为此时的count是局部变量,很明显使用nonlocal声明即可解决该问题

3.装饰器

有了装饰器的知识之后就可以实现装饰器,装饰器的本质就是一个函数,只不过其参数也是一个函数,即被装饰的函数,装饰器中一般还会内嵌一个函数,该函数的返回值一般是被装饰函数的值,并且还会执行其他的操作,而装饰器的返回值一般都是内嵌的函数,而支持这一切操作的基础就在于Python中的函数是一等对象以及闭包的存在。同时Python中的装饰器在模块导入的时候就会被执行(FluentPython中有示例),其中的内嵌函数当然此时就会被定义,下面还是用Fluentpython中的装饰器例子

# clockdeco.py


import time

def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter()
        name = func.__name__
        arg_str = ",".join(repr(arg) for arg in args)  # 当生成器表达式是函数的唯一参数时不需要加括号
        print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, arg_str, result))
        return result
    return clocked

 

import time

@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

if __name__  == "__main__":
    print("6!=", factorial(6))
# [0.12334461s] factorial(1) -> 1
# [0.12337134s] factorial(2) -> 2
# [0.12339190s] factorial(3) -> 6
# [0.12340990s] factorial(4) -> 24
# [0.12342892s] factorial(5) -> 120
# [0.12344794s] factorial(6) -> 720
# 6!= 720

函数执行的过程为:程序执行的过程:在模块加载的时候首先执行装饰器@clock,该装饰器中定义的函数clock(*args),返回该函数之后factorial即指向该函数,并且此时闭包保存了func的值,即原来的factorial。之后执行factorial(6)的时候相当于执行clocked(6),此时,当执行到result = func(*args)的时候,此时的func即为闭包中保存的原来的factorial(求阶乘),此时的*args即为6,所以会执行func(6)=>return 1 if n < 2 else n*factorial(n-1)=>此时factorial是指向clocked的,所以又会执行clocked(5)..,如此递归,直到n=1的时候函数返回,此时会依次从n=1一直往上输出,因为clocked()中func下面的语句只有当func函数返回之后才会执行,所以最终的输出结果如上所示!

点击查看上述代码的详细执行过程!

你可能感兴趣的:(Python)