前置知识
*args **kwargs
- 形参:
- *args 接收参数组成元组
- **kwargs 接收参数组成字典
- 实参:
- *args 将可迭代对象分解出来作为一个个的值进行传递
- **kwargs 将可迭代对象分解出来作为一组组的值进行传递
def foo(a, b):
print(a, b)
def func(*args, **kwargs):
foo(*args, **kwargs)
func(11, 222)
func(111, b=22)
func(a=1111, b=2222)
func(b=88, a=99)
11 222
111 22
1111 2222
99 88
名称空间与作用域
名称空间的“嵌套”关系是在函数定义阶段,即检测语法的时候确定的
函数对象
即可以把函数当做参数传入,也可以把函数当做返回值返回
函数的嵌套
函数内部又定义了一个函数
def outter():
def inner():
pass
return inner
闭包函数
闭包函数:
函数内定义了一个新的函数
函数内的函数,引用了外层函数的变量
def outter(x):
def inner():
print(x)
return inner
inner = outter(1)
什么是函数装饰器
函数装饰器是用来为其他函数增加额外功能的
为什么要用装饰器
开放封闭原则是面向对象原则的核心之一。
开放,指扩展功能是开放的;
封闭,指修改源代码是封闭的;
装饰器就是在不修改被装饰对象源代码以及调用方式的前提下为被装饰对象添加新功能
装饰器的使用
下面以一个小例子来演变演示如何对已开发完成的功能通过装饰器的方式追加新功能
需求:写一个加法运算函数。在不修改加法运算函数源代码及调用方式的基础上增加加法运算时长统计功能。(由于加法计算耗时极短,为方便时长统计更直观,在原需求上增加休眠时间2秒time.sleep(2)
)
写一个简单的加法运算函数
def func_sum(a, b): result = a + b print(result) func_sum(1, 3)
增加休眠时间2秒
import time def func_sum(a, b): time.sleep(2) result = a + b print(result) func_sum(1, 3)
即我们只计算这3行代码的执行耗时
time.sleep(2) result = a + b print(result)
分析需求,在统计上面3行代码的运行耗时时长,有两种考虑:
a. 在加法函数中增加统计代码
import time def func_sum(a, b): start = time.time() time.sleep(2) result = a + b print(result) stop = time.time() print(stop - start) func_sum(1, 3) 2.0047240257263184 4
这样的实现方式,虽然实现了运行时长统计功能,也没有变更函数的调用方式,但修改了函数自身的源代码,不满足需求,即开放封闭原则。
b. 在调用函数的代码处增加统计代码
def func_sum(a, b): time.sleep(2) result = a + b print(result) start = time.time() func_sum(1, 3) stop = time.time() print(stop - start) 4 2.003887891769409
这样的实现方式,虽然实现了运行时长统计功能,即没有改动函数的源代码,也没有修改函数调用方式,但这个加法函数会在多次位置进行调用,每调用一次,就会增加
start
、stop
、stop-start
三行代码,造成代码冗余。上面的b的可以实现功能,但有代码重复的可能,这里将可能重复的代码写成函数
import time def func_sum(a, b): time.sleep(2) result = a + b print(result) def time_count(): start = time.time() func_sum(1, 3) stop = time.time() print(stop - start) time_count() 4 2.004560708999634
上面的代码虽然解决了多次调用代码冗余的问题,不但改变了原函数
func_sum
的调用方式,而且def time_count()
函数被写死了——func_sum(1,3)
,先做如下调整import time def func_sum(a, b): time.sleep(2) result = a + b print(result) def time_count(a,b): start = time.time() func_sum(a,b) stop = time.time() print(stop - start) time_count(1,3)
这里
func_sum(a,b)
只有两个参数,有可能会有增加参数的可能。并且函数time_count
是一个统计某个函数运行时长的功能类,但这里写死了只能处理func_sum(a,b)
,即需要将函数名写活,也要将参数个数写活。先用*args
和**kwargs
写活参数import time def func_sum(a, b): time.sleep(2) result = a + b print(result) def time_count(*args,**kwargs): start = time.time() func_sum(*args,**kwargs) stop = time.time() print(stop - start) time_count(1,3)
然后再将函数名称写活,也就是为
time_count
这个函数传一个函数名的参数,这里使用闭包函数的概念为time_count
函数传参import time def func_sum(a, b): time.sleep(2) result = a + b print(result) def func1(func_sum): def time_count(*args,**kwargs): start = time.time() func_sum(*args,**kwargs) stop = time.time() print(stop - start) return time_count # time_count函数原本是全局的函数,为了传入一个处理的函数名而通过被func1包上后,需要将time_count本身即内存地址返回到全局 test = func1(func_sum) test(1,3)
修改之前执行计算统计运行时长是
func_sum(1,3)
,而现在是test(1,3)
,可以将func1(func_sum)
赋值给变量time_count
import time def func_sum(a, b): time.sleep(2) result = a + b print(result) def func1(func_sum): def time_count(*args,**kwargs): start = time.time() func_sum(*args,**kwargs) stop = time.time() print(stop - start) return time_count # time_count函数原本是全局的函数,为了传入一个处理的函数名而通过被func1包上后,需要将time_count本身即内存地址返回到全局 func_sum = func1(func_sum) func_sum(1,3) 4 2.0046589374542236
上面的代码已经实现了在不修改函数的源代码和调用方式,为函数增加运行时长统计的功能。上面例子中的变量及函数名按照python的规范惯例做一下调整替换
import time def func_sum(a, b): time.sleep(2) result = a + b print(result) def outter(func): def wrapper(*args,**kwargs): start = time.time() func(*args,**kwargs) stop = time.time() print(stop - start) return wrapper # time_count函数原本是全局的函数,为了传入一个处理的函数名而通过被func1包上后,需要将time_count本身即内存地址返回到全局 func_sum = outter(func_sum) func_sum(1,3)
为
func_sum
函数增加返回值,然后执行并打印返回值import time def func_sum(a, b): time.sleep(2) result = a + b print(result) return result def outter(func): def wrapper(*args,**kwargs): start = time.time() func(*args, **kwargs) stop = time.time() print(stop - start) return wrapper func_sum = outter(func_sum) res = func_sum(1,3) print(res) 4 2.0043609142303467 None
return
的值为None
,需要在wrapper
函数中增加return
返回值import time def func_sum(a, b): time.sleep(2) result = a + b print(result) return result def outter(func): def wrapper(*args,**kwargs): start = time.time() res = func(*args,**kwargs) stop = time.time() print(stop - start) return res return wrapper func_sum = outter(func_sum) res = func_sum(1,3) print(res) 4 2.004016876220703 4
增加减法函数
func_sub
,为减法函数func_sub
添加统计运行耗时的方法import time def func_sum(a, b): time.sleep(2) result = a + b print(result) return result def func_sub(a, b): time.sleep(2) result = a - b print(result) return result def outter(func): def wrapper(*args,**kwargs): start = time.time() res = func(*args,**kwargs) stop = time.time() print(stop - start) return res return wrapper func_sum = outter(func_sum) res1 = func_sum(1,3) print(res1) func_sub = outter(func_sub) res2 = func_sub(333,111) print(res2) 4 2.0013039112091064 4 222 2.0044240951538086 222
这样就完整实现了通过装饰器对函数功能增加的需求
通过简便的方式实现装饰器(语法糖)
先定义装饰器
outter
,定义好要被装饰的函数func_sum
和func_sub
,在两个函数的上方写代码@outter
,表示执行outter
函数,将被装饰的函数及参数传入装饰器进行执行。
import time
def outter(func):
def wrapper(*args,**kwargs):
start = time.time()
res = func(*args,**kwargs)
stop = time.time()
print(stop - start)
return res
return wrapper
@outter
def func_sum(a, b):
time.sleep(2)
result = a + b
print(result)
return result
@outter
def func_sub(a, b):
time.sleep(2)
result = a - b
print(result)
return result
# func_sum = outter(func_sum)
res1 = func_sum(1,3)
print(res1)
# func_sub = outter(func_sub)
res2 = func_sub(333,111)
print(res2)
2.00314998626709
4
222
2.00166392326355
222
无参装饰器模板
通过上面的学习,整理出下面的无参装饰器模板:
def outter(func): # 装饰器函数,func为被装饰函数
def wrapper(*args,**kwargs):
"""被装饰函数前需要添加的内容"""
res = func(*args,**kwargs) # 被装饰函数
"""被装饰函数后需要添加的内容"""
return res
return wrapper
上面的模板还有弊端,函数的属性还没有改。比如__name__
、__doc__
、help
等
最终的无参数装饰器的模板如下:
from functools import wraps
def outter(func): # 装饰器函数,func为被装饰函数
@wraps(func) # 增加wraps语法糖,将原函数的属性赋值给wrapper函数
def wrapper(*args,**kwargs):
"""被装饰函数前需要添加的内容"""
res = func(*args,**kwargs) # 被装饰函数
"""被装饰函数后需要添加的内容"""
return res
return wrapper
有参装饰器
使用装饰器后:
- 原函数的参数什么样,wrapper的参数就应该是什么样;
- 原函数的返回值什么样,wrapper的返回值就应该是什么样;
- 原函数的属性什么样,wrapper的属性就应该是什么样;
from functools import wraps
def outter(func): # 装饰器函数,func为被装饰函数
@wraps(func) # 增加wraps语法糖,将原函数的属性赋值给wrapper函数
def wrapper(*args,**kwargs):
"""被装饰函数前需要添加的内容"""
res = func(*args,**kwargs) # 被装饰函数
"""被装饰函数后需要添加的内容"""
return res
return wrapper
基于上面的代码,上面已经介绍wrapper()已经无法增加参数了,但outter()函数是可以增加参数的。
若outter()不作为语法糖去装饰别的函数,是可以增加参数的。若outter()作为语法糖去装饰别的函数,就无法增加参数了,所以不建议给outter()增加参数。
若增加一个参数outter(func,x),当代码执行到语法糖的时候,被语法糖装饰的函数会将函数名以参数的形式传给outter(index),而outter(func,x)新增的参数x没有传值的地方,就会报错
Traceback (most recent call last):
File "/Users/laobai/test3.py", line 24, in <module>
@outter
TypeError: outter() missing 1 required positional argument: 'x'
基于这样的前提下,若要想再为wrapper传递别的参数,要如何解决呢?下面进行实例演示
a.实现一个简单的访问首页的函数
def index(x,y):
print('index -->> %s,%s'%(x,y))
index(1,2)
b.为这个函数增加权限认证,有权限的才能执行这段程序,通过装饰器来实现
通过无参装饰器的模板实现如下:
from functools import wraps
def auth(func): # 装饰器函数,func为被装饰函数
@wraps(func) # 增加wraps语法糖,将原函数的属性赋值给wrapper函数
def wrapper(*args,**kwargs):
"""被装饰函数前需要添加的内容"""
name = input('your name>>:').strip()
pwd = input('your password>>:').strip()
if name == 'laobai' and pwd == '123':
print('login success')
res = func(*args,**kwargs) # 被装饰函数
"""被装饰函数后需要添加的内容"""
return res
else:
print('user or password error')
return wrapper
@auth
def index(x,y):
print('index -->> %s,%s'%(x,y))
index(1,2)
上面代码写死了账号信息
执行结果(有权限)
your name>>:laobai
your password>>: 123
login success
index -->> 1,2
执行结果(没权限)
your name>>:lb
your password>>:123
user or password error
c.在上面的代码基础上,增加两个功能home()和navigation()
代码实现如下:
def auth(func): # 装饰器函数,func为被装饰函数
@wraps(func) # 增加wraps语法糖,将原函数的属性赋值给wrapper函数
def wrapper(*args,**kwargs):
"""被装饰函数前需要添加的内容"""
name = input('your name>>:').strip()
pwd = input('your password>>:').strip()
if name == 'laobai' and pwd == '123':
print('login success')
res = func(*args,**kwargs) # 被装饰函数
"""被装饰函数后需要添加的内容"""
return res
else:
print('user or password error')
return wrapper
@auth
def index(x,y):
print('index -->> %s,%s'%(x,y))
@auth
def home(name):
print('home -->> %s'%(name))
@auth
def navigation():
print('navigation bar')
index(1,2)
home('laobai')
navigation()
执行结果如下:
your name>>:laobai
your password>>:123
login success
index -->> 1,2
your name>>:laobai
your password>>:123
login success
home -->> laobai
your name>>:laobai
your password>>:123
login success
navigation bar
d.上面的程序中写死了有权限的登录名和密码,这里模拟3个功能,分别从三个位置获取用户名和密码:文件、数据库、ldap
from functools import wraps
def auth(func):
@wraps(func)
def wrapper(*args,**kwargs):
name = input('your name>>:').strip()
pwd = input('your password>>:').strip()
if data_type == 'file':
print('从文件中获取账号密码进行验证')
if name == 'laobai' and pwd == '123':
res = func(*args,**kwargs) # 被装饰函数
return res
else:
print('user or password error')
elif data_type == 'mysql':
print('从mysql数据库中获取账号密码进行验证')
elif data_type == 'ldap':
print('从ldap中获取账号密码进行验证')
else:
print("不支持该data_type")
return wrapper
@auth
def index(x,y):
print('index -->> %s,%s'%(x,y))
@auth
def home(name):
print('home -->> %s'%(name))
@auth
def navigation():
print('navigation bar')
index(1,2)
home('laobai')
navigation()
e.上面代码,模拟了从三种不同位置获取账号信息的场景,但引入了新的变量data_type,上面的代码还没有实现传入调用。之前已经确定了outter()也就是这里的auth()及wrapper()均无法再增加传参了。
当一个函数需要一个参数的时候,有两种方案:1.使用传参的形式;2.在外面再包一层函数,即使用装饰器的形式;
from functools import wraps
def auth(data_type):
def outter(func):
@wraps(func)
def wrapper(*args,**kwargs):
name = input('your name>>:').strip()
pwd = input('your password>>:').strip()
if data_type == 'file':
print('从文件中获取账号密码进行验证')
if name == 'laobai' and pwd == '123':
res = func(*args,**kwargs) # 被装饰函数
return res
else:
print('user or password error')
elif data_type == 'mysql':
print('从mysql数据库中获取账号密码进行验证')
elif data_type == 'ldap':
print('从ldap中获取账号密码进行验证')
else:
print("不支持该data_type")
return wrapper
return outter
@auth(data_type='file')
def index(x,y):
print('index -->> %s,%s'%(x,y))
@auth(data_type='mysql')
def home(name):
print('home -->> %s'%(name))
@auth(data_type='ldap')
def navigation():
print('navigation bar')
index(1,2)
home('laobai')
navigation()
执行结果:
your name>>:laobai
your password>>:123
从文件中获取账号密码进行验证
index -->> 1,2
your name>>:laobai
your password>>:123
从mysql数据库中获取账号密码进行验证
your name>>:laobai
your password>>:123
从ldap中获取账号密码进行验证
上面的代码,通过对auth()语法糖中进行传参data_type给auth()函数,代入的新的参数后,再通过装饰器outter为index()做为语法糖@outter
可以简单理解为:
@auth(data_type='file')
def index(x,y):
print('index -->> %s,%s'%(x,y))
等价于
outter=auth(data_type='file')
@outter
def index(x,y):
print('index -->> %s,%s'%(x,y))
根据上面,总结有参装饰器模板:
from functools import wraps
def 有参装饰器(x,y,z):
def outter(func): # 装饰器函数,func为被装饰函数
@wraps(func) # 增加wraps语法糖,将原函数的属性赋值给wrapper函数
def wrapper(*args,**kwargs):
"""被装饰函数前需要添加的内容"""
res = func(*args,**kwargs) # 被装饰函数
"""被装饰函数后需要添加的内容"""
return res
return wrapper
return outter
@有参装饰器(1,2,z=3)
def 被装饰函数():
pass
叠加使用装饰器
def deco1(func1): # func1 = wrapper2的内存地址
def wrapper1(*args,**kwargs):
print("正在运行===>deco1.wrapper1")
res1 = func1(*args,**kwargs)
return res1
return wrapper1
def deco2(func2): # func2=wrapper3的内存地址
def wrapper2(*args,**kwargs):
print("正在运行===>deco2.wrapper2")
res2 = func2(*args,**kwargs)
return res2
return wrapper2
def deco3(x):
def outter3(func3): # func3=被装饰对象index函数的内存地址
def wrapper3(*args,**kwargs):
print("正在运行===>deco3.outter3.wrapper3")
res3 = func3(*args,**kwargs)
return res3
return wrapper3
return outter3
# 加载顺序是从下到上
@deco1 # index=deco1(wrapper2的内存地址) ===> index=wrapper1的内存地址
@deco2 # index=deco2(wrapper3的内存地址) ===> index=wrapper2的内存地址
@deco3(111) # ===> @outter3 ===> index=outter3(index) ===> index= wrapper3的内存地址
def index(x,y):
print('from index %s:%s' %(x,y))
# 执行顺序自上而下,即wrapper1 --> wrapper2 --> wrapper3
index(1,3)
执行结果:
正在运行===>deco1.wrapper1
正在运行===>deco2.wrapper2
正在运行===>deco3.outter3.wrapper3
from index 1:3