【python基础】怎么实现装饰器?
装饰器
什么是装饰器
装饰器是为函数和类指定管理或扩增代码的一种方式;
装饰器本身采取可调用对象的形式(如函数),并处理其他可调用对象。
换句话说,装饰器提供一种方法,在函数和类定义语句结束时插入自动运行的代码,对于函数装饰器,在def
语句结束时插入,对于类装饰器,在class
语句结束时插入。
装饰器通过在def
和class
语句的末尾自动把函数名和类名重绑定到其他可调用对象上来实现这些效果。
为什么使用装饰器
装饰器本质上是一个python
函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景比如:
插入日志、性能测试、事务处理、缓存、权限校验等场景
优点:
它可以使得意图更加明确、能够减少冗余性、复用代码、促进代码封装
缺点:
当它们插入包装器逻辑,可以修改所装饰对象的类型,当用作调用 或接口代理时,它们可能引发额外的调用
怎么实现装饰器
在讲怎么实现装饰器之前,我们先来了解一下什么是闭包
闭包
闭包又叫工厂函数,有时用于时间处理器程序,这些程序需要对运行时的情况做出即时响应。
闭包是指定义一个外层函数,用来简单生成并返回一个嵌套的内函数,却不调用这个内函数,外层函数的返回值是内函数的引用。也就是说,当我们调用外层函数时,内函数会被创建并被返回。
- 外层函数的内部定义了一个内函数
- 内函数使用了外层函数的临时变量
- 外函数的返回值是内函数的引用
举个例子:
1、当我们只是在调用外层函数maker
时,maker
函数创建了action
函数并被返回,但是action
函数并不会被执行,action
函数是在内嵌的def
运行时创建的;
def maker(n):
def action(x):
return x**n
return action
f = maker(2)
print(f)
output >>>
<function maker.<locals>.action at 0x000001AE8FEE70D0>
2、如果我们调用从外层函数得到的返回值
print(f(3))
output >>>
9
虽然在调用action
是maker
已经返回了值并退出,但是内嵌的函数记住了整数2
,即maker
内部的变量n的值。实际上,在外层嵌套局部作用域内的n
被作为执行的状态信息保留下来,并附加到生成的action
函数上,这也是当它被调用时我们返回其参数平方的原因。换言之,我们在调用外层函数maker
传入的3
,此时返回的就是参数的立方。
3、嵌套作用域的另一种表现lambda函数创建表达式
def maker(n):
return lambda x: x ** n
f = maker(2)
print(f)
output >>>
<function maker.<locals>.<lambda> at 0x0000026E5ECC70D0>
print(f(2))
output >>>
9
装饰器的实现
使用@语法糖
:
在def
语句结束时通过另一个函数来运行这个函数,把最初的函数名重新绑定到返回的结果。
装饰器在定义函数或方法的def
语句的前一行编写,并且它由@
符号以及紧随其后的对于元函数的一个引用组成,起到一个管理另一个函数(或其他的可调用对象)的作用。
装饰器简单实现
def use_log(func):
def wrapper():
print("%s is running" % func.__name__)
return func()
return wrapper
@use_log
def say_hello_world():
print("hello world!")
# 上面的写法等同于,因为写法不太优雅,后面引入@语法糖
# say_hello_world = use_log(say_hello_world)
say_hello_world()
output >>>
say_hello_world is running
hello world!
被装饰的函数需要传入参数
# 被装饰的函数传入一个参数
def use_log(func):
def wrapper(n):
print("%s is running" % func.__name__)
return func(n)
return wrapper
@use_log
def say_hello_world(n):
print("hello world! " * n)
say_hello_world(2)
output >>>
say_hello_world is running
hello world! hello world!
# 被装饰的函数传入两个参数
import time
def use_log(func):
def wrapper(x, y):
start_time = time.time()
func(x , y)
end_time = time.time()
msecs = (end_time - start_time) * 1000
print("this func : {} running time is {}".format(func.__name__, msecs))
return wrapper
@use_log
def say_hello_world(x, y):
print("square: {}".format(x * y))
say_hello_world(2, 3)
output >>>
square: 6
this func : say_hello_world running time is 0.0
被装饰的函数需要传入可变参数
因为不确定你要装饰的函数需要传入多少个参数,于是
def use_log(func):
def wrapper(*args, **kwargs):
print("%s is running" % func.__name__)
return func(*args, **kwargs)
return wrapper
@use_log
def say_hello_world(n):
print("hello world! " * n)
@use_log
def maker(n, x):
print("{}".format(n**x))
say_hello_world(2)
output >>>
say_hello_world is running
hello world! hello world!
maker(2,3)
output >>>
maker is running
8
带参数的装饰器
def use_log(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "info":
print("{} is running".format(func.__name__))
return func(*args, *kwargs)
return wrapper
return decorator
@use_log(level="info")
def say_hello_world(name):
print("I am %s" % name)
# 等同于 say_hello_world = use_log(level="info")(say_hello_world)
say_hello_world("Imobs")
output >>>
say_hello_world is running
I am Imobs
将一个函数的返回值传入另一个函数
def depend_case(func1):
def decorator(func):
def warpper(*args, **kwargs):
res_func1 = func1()
if res_func1 is None:
print("func1 返回值为空")
if isinstance(res_func1, dict):
return func(res_func1, *args, **kwargs)
else:
print("func1 返回格式不合法")
return warpper
return decorator
def case_01():
return {'name': 'pophas', 'age': '28'}
@depend_case(func1=case_01)
def case_02(attr, name):
print(f'case_01的返回值: {attr},case_02传入的name:{name}')
case_02("hello")
output >>>
case_01的返回值: {'name': 'pophas', 'age': '28'},case_02传入的name:hello
实际上是对原有装饰器的一个函数封装,并返回一个装饰器,这里可以理解成一个含有参数的闭包,当我们使用@use_log(level="info")
调用的时候,可以将参数传递到装饰器中。
类装饰器
def decorator(C):
return C
@decorator
class C:
...
为了插入一个包装器层来拦截随后的实例创建调动,可返回一个不同的可调用对象。
上面的一个类装饰器返回的可调用对象,通常创建并返回最初的类的一个新实例,并以某种方式扩展以管理其接口。
例如:下面的装饰器插入一个对象来拦截类实例的未定义属性:
def decorator(cls):
class Wrapper:
def __init__(self, *args):
self.wrapper = cls(*args)
def __getattr__(self, name):
return getattr(self.wrapper, name)
return Wrapper
@decorator
class C:
def __init__(self, x, y):
self.attr = 'spam'
x = C(6, 7)
print(x.attr)
output >>>
spam
装饰器把类的名称重新绑定到另一个类,这个类在外层作用域中保持了最初的类,并且当调用的时候,这个类创建并嵌入了最初类的一个实例。
当之后从该实例获取一个属性的时候,包装器的__getattr__
拦截了它,并且将其委托给最初的类的嵌入实例。
此外,每个被装饰的类都创建一个新的作用域,它记住了最初的类。
类装饰器会拦截实例创建调用(每次创建都运行__call__
方法,当使用@
形式将装饰器绑定到函数上时,就会调用这个方法。)
在python
中一般callable
对象都是函数,但是某个对象重载了__call__
方法(前后带下划线被称为内置方法或者魔法方法,而重载魔法方法一般会改变对象的内部行为;),那么这个对象也是callable
的。
tips1:
class useing(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("enter function {func}()".format(func=self.func.__name__))
return self.func(*args, **kwargs)
@useing
def funcg(attr):
print("hello {}!".format(attr))
funcg("Imobs")
output >>>
enter function funcg()
hello Imobs!
- 在
tips1
例子中,当使用useing
来对funcg
函数进行装饰的时候,首先会创建useing
的实例对象,并且会把funcg
这个函数名当做参数传递到__init__
方法中,即在__init__
方法中的func
变量指向了funcg
函数体。 funcg
函数相当于指向了用useing
创建出来的实例对象。- 当在使用
funcg()
进行调用时,就相当于让这个对象实例化,因此会调用这个对象的__call__
方法 - 为了能够在
__call__
方法中调用原来的funcg
指向的函数体,所以在__init__
方法中就需要一个实例属性来保存这个函数体的引用,所以才有了self.func = func
这行代码,从而在调用__call__
方法中能够调用到funcg
之前的函数体。
tips2:
class tracer:
def __init__(self, func):
self.calls = 0
self.func = func
def __call__(self, *args, **kwargs):
self.calls += 1
print('call %s to %s' % (self.calls, self.func.__name__))
return self.func(*args, **kwargs)
@tracer
def spam(a, b, c):
print(a + b + c)
@tracer
def eggs(x, y):
print(x ** y)
spam(1, 2, 3)
spam(2, 3, 4)
spam(a=4, b=5, c=6)
#
eggs(2, 2)
eggs(4, y=4)
output >>>
call 1 to spam
6
call 2 to spam
9
call 3 to spam
15
call 1 to eggs
4
call 2 to eggs
256
tips3:
计算耗时很长的函数,并且每次计算的结果不变
,那么我们可以通过类定义一个缓存装饰器,来缓存第一次执行的结果
import time
class Cache:
__cache = {}
def __init__(self, func):
self.func = func
def __call__(self):
# 如果缓存字典中有这个方法的执行结果
# 直接返回缓存的值
if self.func.__name__ in Cache.__cache:
return Cache.__cache[self.func.__name__]
# 计算方法的执行结果
value = self.func()
# 将其添加到缓存
Cache.__cache[self.func.__name__] = value
# 返回计算结果
return value
@Cache
def long_time_func():
time.sleep(5)
return '计算结果:'
start = time.time()
print(long_time_func())
end = time.time()
print(f'计算耗时{end-start}秒')
start = time.time()
print(long_time_func())
end = time.time()
print(f'计算耗时{end-start}秒')
output >>>
计算结果:
计算耗时5.000198602676392秒
计算结果:
计算耗时0.0秒
带参数的类装饰器
带参数的类装饰器与不带参数的类装饰器有点区别:
首先构造函数__init__
接收的不是一个函数,而是传入的参数,通过类将这些参数保存起来,然后在重载__call__
方法就需要接收一个函数并返回了。
class UseLog(object):
def __init__(self, level='info'):
self.level = level
def __call__(self, func):
def wrapper(*args, **kwargs):
print("this function {func}()".format(func=func.__name__))
func(*args, **kwargs)
return wrapper
@UseLog(level='info')
def say(something):
print("say {}!".format(something))
say("mobs")
内置装饰器
常用的装饰器:
property
、staticmethod
、classmethod
,这三个装饰器都是作用于类方法之上的。
-
property 装饰器
property
装饰器用于类中的函数,使得我们可以像访问属性一样来获取一个函数的返回值。属性有三个装饰器:
setter
,getter
,deleter
,都是在property()
的基础上做了一些封装,,因为setter
和deleter
是property()
的第二和第三个参数,不能直接套用@
语法。getter
装饰器和不带getter
的属性装饰器效果是一样的,经过@property
装饰过的函数返回的不再是一个函数,而是一个property
对象。class XiaoMing: first_name = '明' last_name = '小' @property def full_name(self): return self.last_name + self.first_name xiaoming = XiaoMing() print(xiaoming.full_name) output >>> 小明
如上的例子中,我们可以像获取属性一样获取
full_name
方法的返回值,这就是使用property
装饰器的意义,既能像属性一样获取值,又可以获取值的时候做一些操作; -
staticmethod 装饰器
staticmethod
装饰器同样是用于类中的方法,这表示这个方法将会是一个静态方法,意味着该方法可以直接被调用无需实例化,但同样意味着它没有self
参数,也无法访问实例化后的对象。class XiaoMing: @staticmethod def say_hello(): print('同学你好') XiaoMing.say_hello() # 实例化调用也是同样的效果 # 也就是说:使用了静态方法后,类有没有实例化的效果都是等同的 xiaoming = XiaoMing() xiaoming.say_hello() output >>> 同学你好 同学你好
-
classmethod 装饰器
classmethod
依旧是用于类中的方法,这表示这个方法将会是一个类方法,意味着该方法可以直接被调用无需实例化,但同样意味着它没有self
参数,也无法访问实例化后的对象。相对于staticmethod
的区别在于它会接收一个指向类本身的cls
参数。class XiaoMing: name = '小明' @classmethod def say_hello(cls): print('同学你好, 我是' + cls.name) print(cls) XiaoMing.say_hello() output >>> 同学你好, 我是小明 <class '__main__.XiaoMing'>
注意:@staticmethod
和@classmethod
都是调用的是各自的__init__()
构造函数
多个装饰器的执行顺序
@a
@b
@c
def f ():
...
# 等同于
f = a(b(c(f)))
也就是按照靠近原函数顺序执行,如上面的例子:c -> b -> a
tips1:
def a(func):
print('----1----')
def wrapper():
print('----2----')
func()
print('----3----')
return wrapper
def b(func):
print('----a----')
def inner():
print('----b----')
func()
print('----c----')
return inner
# @a相当于test = a(test) 装饰时候已经执行一次外部函数了。
@a
@b
def test():
print('----4----')
test()
output >>>
----a----
----1----
----2----
----b----
----4----
----c----
----3----
@
装饰的是它下面一行的函数,而@a
下面的不是函数,就先装饰@b
如上面tips1
的例子所示:
-
首先按照装饰器函数在被装饰函数定义后,从下往上执行:
@b -> @a
等同test=a(b(test))
,执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器 -
当我们调用
test()
时,inner
被调用了,它会先打印a
,然后在inner
内部调用了wrapper
,所以会在打印1
,然后在wrapper
内部调用原始函数test
,并且将结果作为最终值返回 -
通俗来讲,直接使用套用的方式理解,将
test
套用在inner.func
,然后套用在wrapper.func
-
首先按照装饰器从下往上执行外层函数
def a(func): print('----1----') def wrapper(): ... def b(func): print('----a----') def inner(): ... @a @b def test(): ... output >>> ----a---- ----1----
-
再执行内部函数(包装层),按照套用的方式看
def wrapper(): print('----2----') def inner(): print('----b----') print('----4----') print('----c----') return inner print('----3----') return wrapper output >>> ----2---- ----b---- ----4---- ----c---- ----3----
-
wraps 装饰器
一个函数不止有他的执行语句,还有着 __name__
(函数名),__doc__
(说明文档)等属性,在使用装饰器会导致这些属性被改变。
def decorator(func):
def wrapper(*args, **kwargs):
"""doc of wrapper"""
print('123')
return func(*args, **kwargs)
return wrapper
@decorator
def say_hello():
"""doc of say hello"""
print('同学你好')
print(say_hello.__name__)
print(say_hello.__doc__)
output >>>
wrapper
doc of wrapper
由于装饰器返回了 wrapper
函数替换掉了之前的 say_hello
函数,导致函数名,帮助文档变成了 wrapper
函数的了
解决这一问题的办法是通过 functools
模块下的 wraps
装饰器。
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""doc of wrapper"""
print('123')
return func(*args, **kwargs)
return wrapper
@decorator
def say_hello():
"""doc of say hello"""
print('同学你好')
print(say_hello.__name__)
print(say_hello.__doc__)
output >>>
say_hello
doc of say hello