装饰器
装饰器就是闭包函数的一种应用场景
为何要用装饰器
开放封闭原则:对修改封闭,对扩展开放
什么是装饰器
装饰器他人的器具,本身可以是任意可调用对象,被装饰者也可以是任意可调用对象。
强调装饰器的原则:1 不修改被装饰对象的源代码 2 不修改被装饰对象的调用方式
装饰器的目标:在遵循1和2的前提下,为被装饰对象添加上新功能
装饰器的使用
函数不固定参数,装饰器的使用
import time
def timmer(func):
def wrapper(*args,**kwargs):
start_time=time.time()
res=func(*args,**kwargs)
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))
return res
return wrapper
@timmer # 相当于 foo = timmer(foo)
def foo():
time.sleep(2)
print('from foo')
@timmer # 相当于 foo = timmer(foo)
def foo1(name):
time.sleep(2)
print('from foo', name)
foo()
foo1('weilianxin')
上述的foo经过装饰器装饰后,foo已经相当于wrapper,foo1亦是如此,所以运行foo和foo1相当于运行wrapper,传参也是向wrapper传参。
foo1给timmer(func):,name给了wrapper(*args,**kwargs):,然后传给res=func(*args,**kwargs),原foo1若有返回值,则传给res
有参装饰器的使用
当装饰器带有多个参数的时候, 装饰器函数就需要多加一层嵌套,如果不调用被装饰函数,可以不多加一层(只写两层):
def auth(auth_type):
print("auth func:", auth_type)
def outer_wrapper(func):
print('123456')
def wrapper(*args, **kwargs):
print("wrapper func args:", *args, **kwargs)
if auth_type == "local":
username = input("Username:").strip()
password = input("Password:").strip()
if user == username and passwd == password:
print(" 33[32;1mUser has passed authentication 33[0m")
res = func(*args, **kwargs) # from home
print("---after authenticaion ")
return res
else:
exit(" 33[31;1mInvalid username or password 33[0m")
elif auth_type == "ldap":
print("搞毛线ldap,不会。。。。")
return wrapper
return outer_wrapper
@auth(auth_type="local") # home = wrapper()
def home():
print("welcome to home page")
return "from home"
@auth(auth_type="ldap")
def bbs():
print("welcome to bbs page")
print('===============',home()) # wrapper()
# bbs()
相当于xx = auth("local") home = xx(home)--简单说就是先执行最外层函数,然后剩下和之前一样
正常一层为@timer------(@函数名),两层的时候auth(auth_type="local")返回内层wrapper函数名,也就是相当于@wrapper
auth_type="local"会传参给auth(auth_type),home传给outer_wrapper(func),home若有参数则类似上一种,继续往里传。
又如flask源码中的:
def route(self, rule, **options):
"""Like :meth:`Flask.route` but for a blueprint. The endpoint for the
:func:`url_for` function is prefixed with the name of the blueprint.
"""
def decorator(f):
endpoint = options.pop("endpoint", f.__name__)
self.add_url_rule(rule, endpoint, f, **options)
return f
return decorator
flask的蓝图route源码中的装饰器, 最内层直接返回return f 并没有多加一层处理的函数, 在无需对被装饰函数进行过多处理的时候这是较为方便的做法. route源码中只是对装饰器参数进行了处理.
注意:
装饰器中函数上面的@fun在解释器走到函数定义时会运行代码,运行外层函数:
import time
def timer(func):
print('adfadsfasf')
def deco(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
end = time.time()
print("time is", end-start)
return deco
@timer # test1 = timer(test1)
def test1():
time.sleep(3)
print("it is test1")
@timer
def test2():
time.sleep(3)
print("it is test2")
@timer
def test3(name):
time.sleep(1)
print("it is test3", name)
# test1 = timer(test1)
# test1()
# test2()
# test3("weilianxin")
pass
在调用语句被注释后,依然执行了@timmer,执行了外层函数,输出了三行
adfadsfasf
adfadsfasf
adfadsfasf
多装饰器
多个装饰器的原则:(极其重要)
装饰器的加载顺序是:由下而上,装饰器的执行顺序是:由上而下。
执行从上至下,按顺序执行时,遇到fun(被装饰函数的调用)才会跳转到另一个装饰器继续执行(因为可以别的装饰器函数里也有fun),但是遇到第一个return就直接返回了,不管别的装饰器有没有执行成。
要点说明:
def bold(fun):
print('----a----')
def inner1():
print('----1----')
fun()
print('----1111----')
return "11111111111111"
return inner1
def italic(fun):
print('----b----')
def inner2():
print('----2----')
fun()
print('----2222----')
return "2222222222222"
return inner2
@bold
@italic
def test():
print("123456")
return 'hello python decorator'
ret = test()
print(ret)
装饰器的加载顺序是:由下而上,装饰器的执行顺序是:由上而下,遇到第一个fun()跳转到下一个装饰器,遇到第一个return返回,结果如下:
----b---- ----a---- ----1---- ----2---- # 前四行是顺序 123456 # fun()执行 ----2222---- ----1111---- 11111111111111 # 第一个return
上面的例子是简单的说明要点的重要性
要点说明2:
def bold(fun):
print('----a----')
def inner1():
print('----1----')
# fun()
# print('----1111----')
return fun()
# return fun()相当于fun() 然后return None,因为不写return和return None效果一样,所以这句话就相当于执行fun,return可以忽略
return inner1
def italic(fun):
print('----b----')
def inner2():
print('----2----')
# fun()
# print('----2222----')
return "222222222222"
return inner2
def line(fun):
print('----c----')
def inner3():
print('----3----')
# fun()
# print('----3333----')
return fun()
return inner3
@bold
@italic
@line
def test():
print("123456")
return 'hello python decorator'
ret = test()
print(ret)
在第一个装饰器中遇到fun()跳转到第二个装饰器执行,遇到return返回,下面的都不执行,结果如下:
----c---- ----b---- ----a---- ----1---- ----2---- 222222222222
注意,return fun()相当于fun(),可以理解成先执行fun() 然后return None,因为不写return和return None效果一样,所以这句话就相当于执行fun,return可以忽略
要点说明三:
def bold(fun):
print('----a----')
def inner1():
print('----1----')
return '1111111111111'
return inner1
def italic(fun):
print('----b----')
def inner2():
print('----2----')
return fun()
# return fun()相当于fun() 然后return None,因为不写return和return None效果一样,所以这句话就相当于执行fun,return可以忽略
return inner2
def line(fun):
print('----c----')
def inner3():
print('----3----')
return inner3
@bold
@italic
@line
def test():
print("123456")
return 'hello python decorator'
ret = test()
print(ret)
这里遇到第一个return就已经返回,下面的fun都不执行,结果如下:
----c---- ----b---- ----a---- ----1---- 1111111111111
如果你理解了,可以看看flask的登陆验证装饰器放置的位置在上好还是在下好
flask登陆验证装饰器和路由装饰器:
from flask import Flask, render_template, request, redirect, session, url_for
app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = '123456'
app.config.from_object("settings.DevelopmentConfig")
USERS = {
1: {'name': '张桂坤', 'age': 18, 'gender': '男',
'text': "当眼泪掉下来的时候,是真的累了, 其实人生就是这样: 越不过的无奈,听不完的谎言,看不透的人心放不下的牵挂,经历不完的酸甜苦辣,这就是人生,这就是生活。"},
2: {'name': '主城', 'age': 28, 'gender': '男',
'text': "高中的时候有一个同学家里穷,每顿饭都是膜膜加点水,有时候吃点咸菜,我们六科老师每天下课都叫他去办公室回答问题背诵课文,然后说太晚啦一起吃个饭,后来他考上了人大,拿到通知书的时候给每个老师磕了一个头"},
3: {'name': '服城', 'age': 18, 'gender': '女',
'text': "高中的时候有一个同学家里穷,每顿饭都是膜膜加点水,有时候吃点咸菜,我们六科老师每天下课都叫他去办公室回答问题背诵课文,然后说太晚啦一起吃个饭,后来他考上了人大,拿到通知书的时候给每个老师磕了一个头"},
}
def wapper(func):
def inner(*args, **kwargs):
user = session.get('user_info')
if not user:
return redirect("/login")
return func(*args, **kwargs)
return inner
@app.route('/detail/<int:nid>', methods=['GET'], endpoint='l0') # 配置动态url
@wapper
def detail(nid):
user = session.get('user_info')
if not user:
return redirect('/login')
info = USERS.get(nid) # 获取动态url
return render_template('detail.html', info=info)
@app.route('/index', methods=['GET'])
def index():
user = session.get('user_info')
if not user:
# return redirect('/login')
url = url_for('l1') # url_for可以进行反向解析
return redirect(url)
return render_template('index.html', user_dict=USERS)
@app.route('/login', methods=['GET', 'POST'], endpoint='l1') # endpoint设置url别名
def login():
if request.method == "GET":
return render_template('login.html')
else:
# request.query_string
user = request.form.get('user')
pwd = request.form.get('pwd')
if user == 'alex' and pwd == '123':
session['user_info'] = user # 设置session
# return redirect('http://www.luffycity.com')
return redirect('/index')
return render_template('login.html', error='用户名或密码错误')
if __name__ == '__main__':
app.run()
"""
对象后面加括号调用对象的call方法,
run():
from werkzeug.serving import run_simple
run_simple(host, port, self, **options)
run_simple()的第三个参数是self,是上面实例化的app,所以对象()调用的是对象的call方法
def __call__(self, environ, start_response):
# environ,是请求相关,start_response是响应相关
return self.wsgi_app(environ, start_response)
"""
'''路由:
def route(self, rule, **options):
"""Like :meth:`Flask.route` but for a blueprint. The endpoint for the
:func:`url_for` function is prefixed with the name of the blueprint.
"""
def decorator(f):
endpoint = options.pop("endpoint", f.__name__)
self.add_url_rule(rule, endpoint, f, **options) # 路由最关键的就是执行这句话
return f
return decorator
'''
这里的wrapper装饰器只能放在route装饰器下面,因为wrapper装饰器在if not user时,会直接return,这样,后面的路由就无法添加了,suoyi只能在下面,在下面也有问题,因为wrapper装饰后,函数名变成了inner,这样很多函数名都变成inner,endpoint会重复,所以要指定一下,或者使用装饰器修复。
类的装饰器
介绍如何使用Python的装饰器装饰一个类的方法,同时在装饰器函数中调用类里面的其他方法。以捕获一个方法的异常为例来进行说明。
def catch_exception(origin_func):
def wrapper(self, *args, **kwargs):
try:
u = origin_func(self, *args, **kwargs)
return u
except Exception:
self.revive() #不用顾虑,直接调用原来的类的方法
return 'an Exception raised.'
return wrapper
class Test(object):
def __init__(self):
pass
def revive(self):
print('revive from exception.')
# do something to restore
@catch_exception
def read_value(self):
print('here I will do something.')
# do something.
test = Test()
test.read_value()
注意装饰器是写在类的定义外面的
装饰器补充:functools.wraps
functools.wraps的作用:
我们在使用 Decorator 的过程中,难免会损失一些原本的功能信息(.__name__等)。直接拿 stackoverflow 里面的栗子
而functools.wraps 则可以将原函数对象的指定属性复制给包装函数对象, 默认有 __module__、__name__、__doc__,或者通过参数选择。代码如下:
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print func.__name__ + " was called"
return func(*args, **kwargs)
return with_logging
@logged
def f(x):
"""does some math"""
return x + x * x
print f.__name__ # prints 'f'
print f.__doc__ # prints 'does some math'
functools.wraps原理解析
预备知识
在了解wraps修饰器之前,我们首先要了解partial和update_wrapper这两个函数,因为在wraps的代码中,用到了这两个函数。
partial
首先说partial函数,在官方文档的描述中,这个函数的声明如下:functools.partial(func, *args, **keywords)。它的作用就是返回一个partial对象,当这个partial对象被调用的时候,就像通过func(*args, **kwargs)的形式来调用func函数一样。如果有额外的 位置参数(args) 或者 关键字参数(*kwargs) 被传给了这个partial对象,那它们也都会被传递给func函数,如果一个参数被多次传入,那么后面的值会覆盖前面的值。
个人感觉这个函数很像C++中的bind函数,都是把某个函数的某个参数固定,从而构造出一个新的函数来。比如下面这个例子:
from functools import partial
def add(x:int, y:int):
return x+y
# 这里创造了一个新的函数add2,只接受一个整型参数,然后将这个参数统一加上2
add2 = partial(add, y=2)
add2(3) # 这里将会输出5
这个函数是使用C而不是Python实现的,但是官方文档中给出了Python实现的代码,如下所示,大家可以进行参考:
def partial(func, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = keywords.copy()
newkeywords.update(fkeywords)
return func(*args, *fargs, **newkeywords)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc
update_wrapper
接下来,我们再来聊一聊update_wrapper这个函数,顾名思义,这个函数就是用来更新修饰器函数的,具体更新些什么呢,我们可以直接把它的源码搬过来看一下:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
wrapper.__wrapped__ = wrapped
return wrapper
大家可以发现,这个函数的作用就是从 被修饰的函数(wrapped) 中取出一些属性值来,赋值给 修饰器函数(wrapper) 。为什么要这么做呢,我们看下面这个例子。
自定义修饰器v1
首先我们写个自定义的修饰器,没有任何的功能,仅有文档字符串,如下所示:
def wrapper(f):
def wrapper_function(*args, **kwargs):
"""这个是修饰函数"""
return f(*args, **kwargs)
return wrapper_function
@wrapper
def wrapped():
"""这个是被修饰的函数"""
print('wrapped')
print(wrapped.__doc__) # 输出`这个是修饰函数`
print(wrapped.__name__) # 输出`wrapper_function`
从上面的例子我们可以看到,我想要获取wrapped这个被修饰函数的文档字符串,但是却获取成了wrapper_function的文档字符串,wrapped函数的名字也变成了wrapper_function函数的名字。这是因为给wrapped添加上@wrapper修饰器相当于执行了一句wrapped = wrapper(wrapped),执行完这条语句之后,wrapped函数就变成了wrapper_function函数。遇到这种情况该怎么办呢,首先我们可以手动地在wrapper函数中更改wrapper_function的__doc__和__name__属性,但聪明的你肯定也想到了,我们可以直接用update_wrapper函数来实现这个功能。
自定义修饰器v2
我们对上面定义的修饰器稍作修改,添加了一句update_wrapper(wrapper_function, f)。
from functools import update_wrapper
def wrapper(f):
def wrapper_function(*args, **kwargs):
"""这个是修饰函数"""
return f(*args, **kwargs)
update_wrapper(wrapper_function, f) # << 添加了这条语句
return wrapper_function
@wrapper
def wrapped():
"""这个是被修饰的函数"""
print('wrapped')
print(wrapped.__doc__) # 输出`这个是被修饰的函数`
print(wrapped.__name__) # 输出`wrapped`
此时我们可以发现,__doc__和__name__属性已经能够按我们预想的那样显示了,除此之外,update_wrapper函数也对__module__和__dict__等属性进行了更改和更新。
wraps修饰器
OK,至此,我们已经了解了partial和update_wrapper这两个函数的功能,接下来我们翻出wraps修饰器的源码:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
没错,就是这么的简单,只有这么一句,我们可以看出,wraps函数其实就是一个修饰器版的update_wrapper函数,它的功能和update_wrapper是一模一样的。我们可以修改我们上面的自定义修饰器的例子,做出一个更方便阅读的版本。
自定义修饰器v3
from functools import wraps
def wrapper(f):
@wraps(f)
def wrapper_function(*args, **kwargs):
"""这个是修饰函数"""
return f(*args, **kwargs)
return wrapper_function
@wrapper
def wrapped():
"""这个是被修饰的函数
"""
print('wrapped')
print(wrapped.__doc__) # 输出`这个是被修饰的函数`
print(wrapped.__name__) # 输出`wrapped`
至此,我想大家应该明白wraps这个修饰器的作用了吧,就是将 被修饰的函数(wrapped) 的一些属性值赋值给 修饰器函数(wrapper) ,最终让属性的显示更符合我们的直觉。