py10 装饰器
装饰器
装饰器就是闭包函数的一种应用场景
为何要用装饰器
开放封闭原则:对修改封闭,对扩展开放
什么是装饰器
装饰器他人的器具,本身可以是任意可调用对象,被装饰者也可以是任意可调用对象。
强调装饰器的原则: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("\033[32;1mUser has passed authentication\033[0m") res = func(*args, **kwargs) # from home print("---after authenticaion ") return res else: exit("\033[31;1mInvalid username or password\033[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) ,最终让属性的显示更符合我们的直觉。