[ Python ] 装饰器详解
1. 什么是装饰器
装饰器本身是函数,是为其他函数添加功能的函数,装饰器的好处就是在不用更改原函数的代码前提下给函数增加新的功能。
装饰器原则:
(1)不能修改被装饰函数的源代码;
(2)不能修改被装饰函数的调用方式
抓住装饰器的两大原则来学习装饰器。装饰器的预备知识:
装饰器 = 高阶函数 + 嵌套函数 + 闭包
2. 什么是高阶函数
(1)函数本身可以赋值给变量,赋值后变量为函数;
(2)允许将函数本身作为参数传入另一个函数;
(3)允许返回一个函数;
以内置函数 abs 求绝对值为例:
>>> abs(-10) 10 >>> a = abs >>> a(-20) 20
内置函数 abs 赋值给变量 a ,在通过变量 a 执行,现在变量 a 和函数 abs 具有同样的功能;
结论:函数本身也可以赋值给变量,即:变量可以指向函数。当变量指向函数时, 变量要可以想函数一样调用
传入函数
变量即可以指向函数,函数的参数也能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称为 高阶函数
def foo(): print('from foo') def test(func): return func # foo 函数作为参数传入另一个函数 t = test(foo) print(t) # 运行结果: # <function foo at 0x0000026A2A4E9048>
在上面的例子中,test 就是一个高阶函数,接收一个函数作为参数,并返回一个函数,最后的打印是函数的内存地址。
尝试通过高阶函数来实现装饰器的功能:
例1 尝试通过装饰器的方式计算 foo 函数的运行时间
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. 嵌套函数
在一个函数中,定义另一个函数
例:这就是一个嵌套函数
def foo(): def test(): print('test.')
说到嵌套函数就一定会提到作用域;
(1)作用域
一个标识符的可见范围,这就是标识符的作用域。一般常说的是变量的作用域
全局作用域(global):在整个程序运行环境中都可见;
局部作用域:在函数、类等内部可见;局部变量使用范围不能超过其所在的局部作用域
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. 闭包的作用
>>> 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 函数运行的时间
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() 使用语法糖的方式:
无参数的装饰器:
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
这样一个无参数的装饰器就完成了, 因为被装饰的函数无需传入任何参数;
有参数的装饰器:
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 含义请参考:函数的参数
有返回值的装饰器:
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)被装饰的函数带参数和返回值
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)装饰器带有参数
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)多个装饰器同时装饰同一个函数
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 函数的时候必须经过登录验证
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
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)写了一个装饰器,在很多函数中都使用了, 如何简单的全部关闭,如何设计装饰器
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()