装饰器与闭包,讲得不错

参考:

https://blog.csdn.net/bqw18744018044/article/details/113720717

 

一、基本概念和作用
1. 装饰器
装饰器(decorate)必须是可调用对象(Callable),其参数是一个函数,称为“被装饰函数”,其输出也是一个函数(或者可调用对象)。一句话:装饰器是处理函数的函数。

2.装饰器的功能
装饰器的功能是,把一个函数转换成另一个函数。假设存在一个定义好的装饰器decorate,那么使用该装饰器的方法如下:

@decorate
def target():
    print('running target()')

上面代码等价于 def target(): print(
'running target()') target = decorate(target)

 

3.定义第一个装饰器

def deco(func):
    def inner():
        print("耗子尾汁")
    return inner

# 装饰器将target替换成了inner
@deco
def target():
    print("年轻人,不讲武德!")

target()

 

输出:

耗子尾汁

 

二、装饰器的执行时机

装饰器在“被装饰函数”定义后立即执行,通常是在导入python模块时完成执行。

def deco(func):
    print("没错!我装饰器已经执行了.")
    return func

@deco
def target():
    print("年轻人,不讲武德!")
    
@deco
def target1():
    print("五连鞭")

 

输出:

没错!我装饰器已经执行了.
没错!我装饰器已经执行了.

 

三、闭包与nonlocal

1. 作用域

b = 6
def f(a):
    print(a)
    print(b)
    b = 9
f(3)

上面的代码在输出3以后会报错,即输出为

3
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-13-83191ed4d718> in <module>()
      4     print(b)
      5     b = 9
----> 6 f(3)

<ipython-input-13-83191ed4d718> in f(a)
      2 def f(a):
      3     print(a)
----> 4     print(b)
      5     b = 9
      6 f(3)

UnboundLocalError: local variable 'b' referenced before assignment

 

原因:python编译器认为b是局部变量,由于定义b在大约b之后,因此报错。

代码可以改为

b = 6
def f(a):
    global b # 指定b是全局b
    print(a)
    print(b)
    b = 9
f(3)

输出:
3
6

 

2. 闭包

闭包:是一种函数,一种延伸了作用域的函数。具体来说,该函数引入了一个即没有在函数中定义、也不是全局变量的变量。

上面的定义有点绕,来看个例子。

def make_averager(): # 一个返回函数averager的函数
    series = []
    
    # 将new_value存储至series中,并计算series的均值
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager

avg = make_averager()
print(avg(10))
print(avg(11))
print(avg(12))

 

输出:

10.0
10.5
11.0

函数averager()引用了其外部函数make_average()的局部变量series,但是按照上面代码中的调用方式avg(10)就应用直接报错,因为随着make_average()调用的结束,局部变量series应该会被销毁,导致函数averager()无法引用到该变量。但是没有报错啊!!!Series并没有被销毁掉!

这就是python中闭包的概念了,函数averager()就是一个闭包,其引用了一个自由变量(free variable)Series。

基于上面的例子和概念介绍,给出两个概念的定义:

 

自由变量(free variable):未在本地作用域绑定的变量。

闭包(closure):一种函数,它会保留定义函数时存在的自由变量绑定,这样调用函数时虽然定义作用域不可用了,但是仍然能使用绑定的自用变量。

 

 

3.nonlocal

下面通过一个例子来说明关键字nonlocal的作用。下面的例子仍然是一个计算平均数的函数,但该函数不在保存历史值,而是保存总值和元素个数。

def make_averager():
    count = 0
    total = 0
    
    def averager(new_value):
        '''
        首先,count += 1等价于count = count + 1;
        其次,count = count + 1说明要给变量count进行赋值,这会将count转换为局部变量(不赋值则不会转换为局部变量);
        最后,为了防止将count当做局部变量,因此使用nonlocal关键字;(类似global)
        '''
        nonlocal count, total
        count += 1
        total += new_value
        return total/count
    
    return averager

avg = make_averager()
avg(10)
avg(5)

输出
7.5

 

闭包例子中不需要使用nonlocal的原因:

由于列表示可变对象,因此调用函数append并不会给变量series进行赋值,所以其不会被当成局部变量。

四、一个有意义的例子:使用装饰器来计算函数的执行时间
装饰器的典型行为:把被装饰函数替换成新函数,二者接收相同的参数,在将被装饰函数的值返回以外,同时做些额外的操作。

1. 定义一个普通的函数

import time

def snooze(seconds):
    """打盹"""
    time.sleep(seconds)
    return "大意了,没有闪!"

print(snooze.__name__)
print(snooze.__doc__)
print(snooze(3))
print(snooze(seconds=3))

输出:
snooze
打盹
大意了,没有闪!
大意了,没有闪!

 

2. 定义一个计算函数执行时间的装饰器

import time
from datetime import datetime

def clock(func):
    def clocked(*args, **kwargs):
        """计时"""
        start_time = datetime.now()
        result = func(*args, **kwargs)
        elapsed = (datetime.now()-start_time).seconds
        return result, elapsed # 返回函数的原始结果和执行时间
    return clocked

# 使用@clock装饰函数snooze
@clock
def snooze(seconds):
    """打盹"""
    time.sleep(seconds)
    return "大意了,没有闪!"

print(snooze.__name__)
print(snooze.__doc__)
print(snooze(3))
print(snooze(seconds=3))

输出:

clocked
计时
('大意了,没有闪!', 3)
('大意了,没有闪!', 3)

 

可以发现:原始函数的__name__和__doc__被替换了,为了不影响原始函数的这两个属性,可以使用装饰器@functools.wraps,如下:

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        """计时"""
        start_time = datetime.now()
        result = func(*args, **kwargs)
        elapsed = (datetime.now()-start_time).seconds
        return result, elapsed
    return clocked

@clock
def snooze(seconds):
    """打盹"""
    time.sleep(seconds)
    return "大意了,没有闪!"

print(snooze.__name__)
print(snooze.__doc__)
print(snooze(3))
print(snooze(seconds=3))

输出:

snooze
打盹
('大意了,没有闪!', 3)
('大意了,没有闪!', 3)

 

五、叠放装饰器

def wake1(func):
    def wakeup(*args, **kwargs):
        result = func(*args, **kwargs)
        return result + "张三:快别睡了.\n"
    return wakeup
    
def wake2(func):
    def wakeup(*args, **kwargs):
        result = func(*args, **kwargs)
        return result + "李四:起来嗨!"
    return wakeup

@wake2
@wake1
def snooze():
    return "我:困了,要睡觉了.\n"

print(snooze())

输出:

我:困了,要睡觉了.
张三:快别睡了.
李四:起来嗨!

 

 

六、参数化装饰器

装饰器默认会把被装饰函数做为第一个参数传递给装饰器,但是如何为装饰器设定其他参数。那就是使用装饰器工厂函数(也就是在装饰器外在套一层函数)。

def wake_factory(level): # 装饰器工厂函数
    def wake_decorator(func): # 装饰器
        def wakeup(*args, **kwargs):
            result = func(*args, **kwargs)
            if level>0:
                result += "张三:快别睡了.\n"
            if level>1:
                result += "李四:起来嗨!"
            return result
        return wakeup
    return wake_decorator

@wake_factory(level=1)
def snooze():
    return "我:困了,要睡觉了.\n"
print("level=1\n" + snooze())

@wake_factory(level=2)
def snooze():
    return "我:困了,要睡觉了.\n"
print("level=2\n" + snooze())


输出:
level=1
我:困了,要睡觉了.
张三:快别睡了.

level=2
我:困了,要睡觉了.
张三:快别睡了.
李四:起来嗨!

 

七、装饰器functools.lru_cache

装饰器lru_cache实现了LRU(Least Recently Used)算法,其能将函数在指定参数下的值存储起来,从而减少函数的执行时间,将存在的值直接返回。

先来看一个斐波那契数列的计算时间

from datetime import datetime

def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

start_time = datetime.now()
fibonacci(36)
elapsed = (datetime.now()-start_time).seconds
print(elapsed)


输出:
5

再看使用装饰器lru_cache后的计算时间

from datetime import datetime
from functools import lru_cache

@lru_cache()
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

start_time = datetime.now()
fibonacci(36)
elapsed = (datetime.now()-start_time).seconds
print(elapsed)


输出:
0

 

八、装饰器functools.singledispatch

python并不直接支持函数重载,但是可以使用装饰器functools.singleispatch来实现函数的重载。具体方法如下例:

from functools import singledispatch

@singledispatch
def show(obj):
    print(obj, type(obj), "obj")
    
# 参数为str的重载
@show.register(str)
def _(text):
    print(text, type(text), "str")
    
# 参数为int的重载
@show.register(int)
def _(n):
    print(n, type(n), "int")

show("点赞")
show(666)
show(66.6)

输出:

点赞 <class 'str'> str
666 <class 'int'> int
66.6 <class 'float'> obj

 

posted @ 2022-04-04 22:51  blcblc  阅读(37)  评论(0编辑  收藏  举报