【Python】【进阶】装饰器与闭包

一、基本概念和作用

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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
闭包装饰器是一种特殊的装饰器,它使用闭包的概念来实现。闭包是指一个函数可以访问并操作其外部函数中定义的变量。在Python中,闭包装饰器可以用于给函数添加额外的功能,同时保持函数的原始定义不变。 引用中的示例展示了装饰器传参的形式。在这个例子中,outer函数是一个装饰器,它将inner函数作为子函数返回,并在inner函数中添加了额外的功能。通过使用@outer装饰器语法,我们可以在add函数上应用outer装饰器,从而在调用add函数时执行装饰器中的代码。 引用中的示例展示了多层装饰器的使用。在这个例子中,outer1和outer2函数分别是两个装饰器,他们都返回一个inner函数。通过使用@outer1和@outer2装饰器语法,我们可以在outers函数上应用这两个装饰器,并在调用outers函数时按照装饰器的定义顺序执行相关的代码。 引用提供了关于Python闭包装饰器的使用方法的总结。这篇文章通过示例代码详细介绍了闭包装饰器的使用,对于学习和工作有一定的参考价值。 引用中的示例展示了装饰器的形式。在这个例子中,outer函数是一个装饰器,它将inner函数作为子函数返回,并在inner函数中添加了额外的功能。通过使用@outer装饰器语法,我们可以在add函数上应用outer装饰器,从而在调用add函数时执行装饰器中的代码。 综上所述,Python闭包装饰器是一种利用闭包概念实现的特殊装饰器,可以用于给函数添加额外的功能。这种装饰器可以通过装饰器传参的形式、多层装饰器的形式或普通的装饰器形式来实现。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BQW_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值