[ Python ] 装饰器详解

1. 什么是装饰器 

装饰器本身是函数,是为其他函数添加功能的函数,装饰器的好处就是在不用更改原函数的代码前提下给函数增加新的功能。

装饰器原则:
    (1)不能修改被装饰函数的源代码;
    (2)不能修改被装饰函数的调用方式

抓住装饰器的两大原则来学习装饰器。装饰器的预备知识:
    装饰器 = 高阶函数 + 嵌套函数 + 闭包

2. 什么是高阶函数

 (1)函数本身可以赋值给变量,赋值后变量为函数;
(2)允许将函数本身作为参数传入另一个函数;
(3)允许返回一个函数;
以内置函数 abs 求绝对值为例:

1
2
3
4
5
>>> abs(-10)
10
>>> a = abs
>>> a(-20)
20

内置函数 abs 赋值给变量 a ,在通过变量 a 执行,现在变量 a  和函数 abs 具有同样的功能;
结论:函数本身也可以赋值给变量,即:变量可以指向函数。当变量指向函数时, 变量要可以想函数一样调用

 

传入函数
变量即可以指向函数,函数的参数也能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称为 高阶函数

1
2
3
4
5
6
7
8
9
10
11
12
def foo():
        print('from foo')
 
def test(func):
        return func
 
# foo 函数作为参数传入另一个函数
t = test(foo)
print(t)
 
# 运行结果:
# <function foo at 0x0000026A2A4E9048>

 

在上面的例子中,test 就是一个高阶函数,接收一个函数作为参数,并返回一个函数,最后的打印是函数的内存地址。

尝试通过高阶函数来实现装饰器的功能:

例1 尝试通过装饰器的方式计算 foo 函数的运行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time
 
def timmer(func):
        start_time = time.time()
        func()
        print('foo函数运行时间:', time.time()-start_time)
 
 
def foo():
        time.sleep(2)
        print('hello, foo.')
 
timmer(foo)
 
# 运行结果:
# hello, foo.
# foo函数运行时间: 2.0004444122314453

 

装饰器是为其他函数添加附加功能的函数。上面的例子中,timmer 函数确实为 foo 添加了附加功能,计算出了 foo 函数运行的时间。
我们在通过装饰器的两大原则来比较:
    (1)不修改被装饰函数的源代码,上面的例子中 foo 函数的源代码没有被修改 -- 满足
    (2)不修改被装饰函数的调用方式; 上面 foo 函数调用方式应该是 foo()  而上面为了实现附加功能,调用方式修改为 timmer(foo),调用方式发生了修改 -- 不满足
所以说,高阶函数不能满足装饰器的两大原则。

3. 嵌套函数

 在一个函数中,定义另一个函数

例:这就是一个嵌套函数

1
2
3
def foo():
        def test():
                print('test.')

 说到嵌套函数就一定会提到作用域;
(1)作用域
一个标识符的可见范围,这就是标识符的作用域。一般常说的是变量的作用域
全局作用域(global):在整个程序运行环境中都可见;
局部作用域:在函数、类等内部可见;局部变量使用范围不能超过其所在的局部作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
def foo():
        name = 'hkey'
        def test():
                print(locals())
                print(name)
        print(locals())
        test()
foo()
 
# 执行结果:
# {'test': <function foo.<locals>.test at 0x000002494DA55950>, 'name': 'hkey'}
# {'name': 'hkey'}
# hkey

 

上面的实例运行步骤如下:
(1)首先打印 foo 函数中 print(locals()) --> {'name': 'hkey', 'test': <function foo.<locals>.test at 0x000001C6F4BF5950>}
打印的是 foo 函数中的局部变量,有变量 name 以及 test 函数的内存地址
(2)执行 foo 函数中的 test() 函数
(3)执行嵌套函数 test 中的 print(locals()) --> {'name': 'hkey'} 发现在嵌套函数 test 中能够获取上一级函数定义的变量
(4)最后在嵌套函数 test 中,打印 name 变量 --> 'hkey'

结论:在嵌套函数中,如果没有定义需要调用局部变量的值,则会去上一层中的局部变量找是否存在,如果上一层不存在,则会去全局变量中找。
就像找一种微量元素,首先在地球上找,如果地球没有则会去太阳系找,如果太阳系没有则会去银河系找。这里面 银河系包括太阳系,太阳系又包括地球

 

4. 闭包的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def foo(n):
...     def test():
...         return n + 1
...     return test
...
>>> f = foo(10)
>>> f
<function foo.<locals>.test at 0x000001D5958EF0D0>
>>> f()
11
>>> f = foo(20)
>>> f()
21

 在这段程序中,函数 test 是函数 foo 的内嵌函数,并且 test 是 foo 函数的返回值。
foo 函数只是返回了内嵌函数 test 的地址,在单独执行 test 函数时将会由于在其作用域中找不到 n 变量而出错。而在函数式语言中,当内嵌函数体内引用到体外的变量时,将会把定义时涉及的引用环境和函数体打包成一个整体返回, 这个实例就是闭包的作用。

 

5. 编写一个装饰器

 通过上面 高阶函数、嵌套函数、闭包的简介及举例,尝试写一个装饰器

例:计算 foo 函数运行的时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time
 
def timmer(func):
        def wrapper():
                start_time = time.time()
                func()
                print('程序运行时间:', time.time()-start_time)
        return wrapper
 
def foo():
        time.sleep(2)
        print('foo run finish.')
 
foo = timmer(foo)
foo()
 
# 运行结果:
# foo run finish.
# 程序运行时间: 2.0003890991210938

 

 (1)首先,定义 timmer函数,定义func为参数,返回值是内嵌函数 wrapper, 被装饰函数为 foo
(2)在定义 foo = timmer(foo) 时,实际上是 foo = wrapper 函数内存地址,这里使用高阶函数和闭包的概念
(3)foo()  等于在执行 wrapper() 在 wrapper 函数中执行了timmer函数中的参数 func , 而这里参数func 就是 foo

 

foo = timmer() 使用语法糖的方式:

无参数的装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
def timmer(func):
        def wrapper():
                start_time = time.time()
                func()
                print('程序运行时间:', time.time()-start_time)
        return wrapper
 
@timmer
def foo():
        time.sleep(2)
        print('foo run finish.')
 
foo()
 
# 运行结果:
# foo run finish.
# 程序运行时间: 2.0003890991210938

 这样一个无参数的装饰器就完成了, 因为被装饰的函数无需传入任何参数;

 

有参数的装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
def timmer(func):
        def wrapper(*args, **kwargs):
                start_time = time.time()
                func(*args, **kwargs)
                print('app runtime:', time.time()-start_time)
        return wrapper
 
@timmer
def foo(name):
        time.sleep(2)
        print('hello,', name)
 
foo('hkey')
 
# 运行结果:
# hello, hkey
# app runtime: 2.0005948543548584

 

在有参数的装饰器中 *args, **kwargs 表示接收了任意类型的参数,关于 *args, **kwargs 含义请参考:函数的参数

 

 有返回值的装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
def timmer(func):
        def wrapper(*args, **kwargs):
                start_time = time.time()
                res = func(*args, **kwargs)
                print('app runtime:', time.time()-start_time)
                return res
        return wrapper
 
@timmer
def foo(name):
        time.sleep(2)
        print('hello,', name)
        return '返回foo'
 
f = foo('hkey')
print(f)
 
# 运行结果:
# hello, hkey
# app runtime: 2.0003833770751953
# 返回foo

 

6. 几种不同的装饰器及执行流程

被装饰函数的视角:

  通过 被装饰函数 的特征分为以下几种类型

    (1)被装饰的函数不带参数和返回值

    (2)被装饰的函数带参数但没有返回值

    (3)被装饰的函数带参数和返回值

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def log(f):
    def wrapper(*args, **kwargs):
        print('func 函数运行前')
        ret = f(*args, **kwargs)
        print('func 函数运行后')
        return ret
    return wrapper
 
 
@log
def func(x, y):
    print('hello', x, y)
    return 'func: 返回值'
 
 
print(func('xiaofei', 'hkey'))
 
 
# 执行结果:
# func 函数运行前
# hello xiaofei hkey
# func 函数运行后
# func: 返回值

 

以上 3 种情况被装饰函数的类型,都可以通过上面的装饰器 log 来装饰。具体执行流程如下图:

 

装饰器视角:
  根据装饰器的使用分为以下

    (1)装饰器带有参数

    (2)多个装饰器装饰一个函数

 

(1)装饰器带有参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def log(text):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print('函数之前执行')
            print('装饰器说明:', text)
            ret = func(*args, **kwargs)
            print('函数之后执行')
            return ret
        return wrapper
    return decorator
 
 
@log('我是带参数的装饰器')
def func(x, y):
    print('hello', x, y)
    return 'func: 返回值'
 
 
print(func('xiaofei', 'hkey'))
 
 
# 执行结果:
# 函数之前执行
# 装饰器说明: 我是带参数的装饰器
# hello xiaofei hkey
# 函数之后执行
# func: 返回值

 

带参数的装饰器执行流程如下:

 

 (2)多个装饰器同时装饰同一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def log1(func): # log(wrapper2)
    def wrapper1(*args, **kwargs):
        print('before: log1')
        ret = func(*args, **kwargs)
        print('after: log1')
        return ret
    return wrapper1
 
def log2(func): # log2(func)
    def wrapper2(*args, **kwargs):
        print('before: log2')
        ret = func(*args, **kwargs)
        print('after: log2')
        return ret
    return wrapper2
 
@log1   # --> func = log1(func) --> log1(wrapper2) 首先执行
@log2   # --> func = log2(func) --> wrapper2
def func():
    print('hello')
 
func()
 
 
# 执行结果:
# before: log1
# before: log2
# hello
# after: log2
# after: log1

 

多个装饰器同时装饰同一个函数流程如下图:

 

 

7. 装饰器的几个实例

 (1)使用装饰器实现登录验证的功能。当用户调用 home 函数的时候必须经过登录验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def auth(func):
        def wrapper(*args, **kwargs):
                username = input('用户名:').strip()
                passwd = input('密码:').strip()
                if username == 'admin' and passwd == '123':
                        res = func(*args, **kwargs)
                        return res
                else:
                        print('用户名密码错误!')
 
        return wrapper
 
@auth
def home():
        print('welcome home.')
 
home()
 
# 执行结果:
# (1)用户名密码正确
# 用户名:admin
# 密码:123
# welcome home.
#
# (2)用户名密码错误
# 用户名:admin
# 密码:111

 

 (2)使用装饰器实现登录功能,当调用 home 函数时,验证方式为 filedb ,当登录购物车时,验证方式为 ldap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def auth(auth_type):
        def decorator(func):
                def wrapper(*args, **kwargs):
                        username = input('用户名:').strip()
                        passwd = input('密码:').strip()
                        if auth_type == 'filedb':
                                print('filedb 验证中...')
                                if username == 'filedb' and passwd == '123':
                                        res = func()
                                        return res
                        elif auth_type == 'ldap':
                                print('ldap 验证中...')
                                if username == 'ldap' and passwd == '123':
                                        res = func()
                                        return res
 
                return wrapper
 
        return decorator
 
@auth('filedb')
def home():
        print('welcome home.')
@auth('ldap')
def shopping_cars():
        print('个人购物车.')
home()
shopping_cars()
 
# 运行结果:
# 用户名:filedb
# 密码:123
# filedb 验证中...
# welcome home.
# 用户名:ldap
# 密码:123
# ldap 验证中...

 

(3)写了一个装饰器,在很多函数中都使用了, 如何简单的全部关闭,如何设计装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Flag = False  # Flag = True的时候使用装饰器中附加功能,否则不使用装饰器中的附加功能
 
 
def log(flag):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if flag:  # 通过 flag 参数来控制是否要使用装饰器中的内容。
                print('----')
                ret = func(*args, **kwargs)
                print('#####')
                return ret
            else:
                ret = func(*args, **kwargs)
                return ret
 
        return wrapper
 
    return decorator
 
 
@log(Flag)
def func1():
    print('func: func1.')
 
 
@log(Flag)
def func2():
    print('func: func2.')
 
 
@log(Flag)
def func3():
    print('func: func3.')
 
 
func1()
func2()
func3()

 

本文作者:hukey

本文链接:https://www.cnblogs.com/hukey/p/9269134.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   hukey  阅读(551)  评论(0编辑  收藏  举报
编辑推荐:
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· .NET 适配 HarmonyOS 进展
阅读排行:
· 用 DeepSeek 给对象做个网站,她一定感动坏了
· DeepSeek+PageAssist实现本地大模型联网
· 手把手教你更优雅的享受 DeepSeek
· 腾讯元宝接入 DeepSeek R1 模型,支持深度思考 + 联网搜索,好用不卡机!
· 从 14 秒到 1 秒:MySQL DDL 性能优化实战
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 彩虹 Jay
彩虹 - Jay
00:00 / 00:00
An audio error has occurred.

彩虹 + 轨迹 (Live) - 周杰伦 (Jay Chou)

彩虹

词:周杰伦

曲:周杰伦

哪里有彩虹告诉我

哪里有彩虹告诉我

能不能把我的愿望还给我

能不能把我的愿望还给我

为什么天这么安静

为什么天这么安静

所有的云都跑到我这里

有没有口罩一个给我

有没有口罩一个给我

释怀说了太多就成真不了

释怀说了太多就成真不了

也许时间是一种解药

也许时间是一种解药

也是我现在正服下的毒药

也是我现在正服下的毒药

看不见你的笑 我怎么睡得着

看不见你的笑 我怎么睡得着

你的声音这么近我却抱不到

你的声音这么近我却抱不到

没有地球太阳还是会绕

没有地球太阳还是会绕

没有理由我也能自己走

没有理由我也能自己走

你要离开 我知道很简单

你要离开 我知道很简单

你说依赖 是我们的阻碍

你说依赖 是我们的阻碍

就算放开 但能不能别没收我的爱

就算放开 但能不能别没收我的爱

当作我最后才明白

当作我最后才明白

看不见你的笑 要我怎么睡得着

看不见你的笑 要我怎么睡得着

你的声音这么近我却抱不到

没有地球太阳还是会绕 会绕

没有理由我也能自己走掉

释怀说了太多就成真不了

也许时间是一种解药 解药

也是我现在正服下的毒药

轨迹

词:黄俊郎

曲:周杰伦

我会发着呆然后忘记你

接着紧紧闭上眼

想着哪一天 会有人代替

想着哪一天 会有人代替

让我不再想念你

我会发着呆 然后微微笑

我会发着呆 然后微微笑

接着紧紧闭上眼

又想了一遍 你温柔的脸

又想了一遍 你温柔的脸

在我忘记之前