装饰器
一、什么是装饰器
如图:由快递
->填充物
-> 快递包裹
构成了我们所收到的快递,我们的快递到我们手里,其实是经过了一层包装装饰后,变成了一个包裹,才到我们手里里,拆开包裹,原始的快递依旧完后,这就是快递包裹的作用。
在程序中,快递就是我们的原始对象,快递包裹就是我们的一个新对象,填充物就是我们的扩展功能,这一系列就构成了装饰器。
由上可得:装饰器是修改其他函数功能的函数,就是一个函数(对象)。
装饰器的意义:让其他函数在不修改任何代码的前提下增加额外功能
二、装饰器的实现原理
我们要从函数的特性->闭包->语法糖,循序渐进的了解装饰器的实现原理
2.1 函数的特性
- 函数作为变量传递
def add(x):
return x+1
a = add # 把函数add当做变量赋值给a
result = a(1)
print(result) # 2
- 函数作为参数传递
def add(x):
return x+1
def excute(func):
"""
:param func 函数名
:return
"""
return func(1)
result = excute(add) # 把add函数当做参数传递给excute函数
print(result) # 2
- 函数作为返回值
def add(x):
return x+1
def get_add():
"""
:return add函数定义
"""
return add
result = get_add()(1) # get_add()->add; get_add()(1)->add(1)
print(result) # 2
- 函数嵌套与跨域访问
def outer(x):
# 函数可以嵌套函数
def inner():
# 嵌套函数中使用了外层函数的局部变量,属于跨域访问
print(x) # 10
inner()
outer(10)
2.2 闭包
闭包就是引用了外部变量的内部函数,闭包的实现就是利用上述四个函数特性。
2.3 语法糖
简而言之,语法糖就是程序语言中提供[奇技淫巧]的一种手段和方式而已。 通过这类方式编写出来的代码,即好看又好用,好似糖一般的语法。固美其名曰:语法糖
我们通过下面的一个例子来了解使用@语法糖
import time
import random
def foo():
time.sleep(random.randint(1, 5))
print("I'm foo fuunction")
我们以实现一个打印上面函数运行时间(不改变原有函数)为例进行详解
- 函数封装
- 可以通过另外定义一个函数进行打印函数的运行时间
def run_time():
start_time = time.time()
foo() # 在新封装的函数中调用需要计算运行时间的函数
end_time = time.time()
time_ = end_time-start_time
print(f"函数的运行时间:{time_}")
# 调用
run_time()
# 结果
"""
I'm foo fuunction
函数的运行时间:4.0052490234375
"""
- 闭包封装
def run_time(func):
def inner():
start_time = time.time()
func()
end_time = time.time()
time_ = end_time-start_time
print(f"函数的运行时间:{time_}")
return inner
# 调用
run_time(foo)() # run_time(foo) -> inner; inner()
# 结果
"""
I'm foo fuunction
函数的运行时间:1.0051140785217285
"""
- 装饰器(@语法糖)
def run_time(func):
def inner():
start_time = time.time()
func()
end_time = time.time()
time_ = end_time-start_time
print(f"函数的运行时间:{time_}")
return inner
@run_time # 这里就是用了@语法糖
def foo():
time.sleep(random.randint(1, 5))
print("I'm foo fuunction")
# 调用
foo()
# 结果
"""
I'm foo fuunction
函数的运行时间:3.0052261352539062
"""
三、装饰器的高级用法
3.1 所装饰的函数带参数
import time
from loguru import logger
def decorate(func):
def wrapper(param):
logger.debug(f"{func.__name__}函数启动时间:{time.time()}")
func(param)
logger.debug(f"{func.__name__}函数结束时间:{time.time()}")
return wrapper
@decorate
def num(x):
print(x)
# 调用
num(1) # decorate(num)(1)
# 结果
"""
2021-09-10 10:26:59.133 | DEBUG | __main__:wrapper:21 - num函数启动时间:1631240819.1332521
1
2021-09-10 10:26:59.133 | DEBUG | __main__:wrapper:23 - num函数结束时间:1631240819.1333902
"""
上面这种方式只适用于只有一个参数的情况,多参数或者没有参数的时候就不再适用,那么定义的这个装饰器的适用性就很差,所以对此装饰器进行进一步的改进
def decorate(func):
def wrapper(*args, **kwargs): # 此处适用的是不确定参数传参(可保证装饰器的通用性)
logger.debug(f"{func.__name__}函数启动时间:{time.time()}")
func(*args, **kwargs)
logger.debug(f"{func.__name__}函数结束时间:{time.time()}")
return wrapper
3.2 所装饰的函数带返回值
import time
from loguru import logger
def decorate(func):
def wrapper(*args, **kwargs):
logger.debug(f"{func.__name__}函数启动时间:{time.time()}")
result = func(*args, **kwargs) # 此处接收函数的返回值
logger.debug(f"{func.__name__}函数结束时间:{time.time()}")
return result # 进行返回
return wrapper
@decorate
def num(x):
return x+1 # 此函数是有返回值(函数默认的返回值为None)
# 调用
result = num(1) # decorate(num)(1)
print(result)
# 结果
"""
2021-09-10 10:28:45.390 | DEBUG | __main__:wrapper:21 - num函数启动时间:1631240925.390035
2021-09-10 10:28:45.390 | DEBUG | __main__:wrapper:23 - num函数结束时间:1631240925.3901389
2
"""
3.3 带参数的装饰器
只需要在普通装饰器的外部再嵌套一层带参数的函数,并且把普通装饰器返回出去即可。
def logging(level):
def log(func):
def wrapper(*args, **kwargs):
print(f"[{level}]: enter {func.__name__}()")
return func(*args, **kwargs)
return wrapper
return log
@logging(level="DEBUG") # 使用装饰器的时候要传递参数,如果不传则会报错
def hello(a, b):
print(a+b)
# 调用
hello(1, 2)
# 结果
"""
[DEBUG]: enter hello()
3
"""
3.4 类装饰器
定义类装饰器,主要使用了__init__
和 __call__
魔法方法
一个对象是否可以加括号使用,取决于是否含有__call__
属性
from typing import Any
class logging(object):
def __init__(self, func):
self.func = func
def __call__(self, *args: Any, **kwds: Any) -> Any:
print(f"[DEBUG]:enter {self.func.__name__}()")
return self.func(*args, **kwds)
@logging
def hello(a, b):
print(a+b)
# 调用
hello("hello ", "world") # logging(hello)("hello ", "world")
# 结果
"""
[DEBUG]:enter hello()
hello world
"""
同样的类装饰器也可以定义参数↓
class logging(object):
def __init__(self, level):
self.level = level
def __call__(self, func) -> Any:
def wrapper(*args: Any, **kwds: Any):
print(f"[{self.level}]:enter {func.__name__}()")
return func(*args, **kwds)
return wrapper
@logging(level="DEBUG")
def hello(a, b):
print(a+b)
# 调用
hello("hello ", "world") # logging(level="DEBUG")(hello)("hello ", "world")
# 结果
"""
[DEBUG]:enter hello()
hello world
"""
3.5 装饰器叠加
def by(func):
print("by:进入了by")
def wrapper(*args, **kwargs):
print("by:开始执行装饰器")
args = int(args[0]), int(args[1])
result = func(*args, **kwargs)
print("by:by装饰器执行完毕")
return result
return wrapper
def captureError(func):
print("captureError:进入了captureError")
def wrapper(*args, **kwargs):
print("captureError:开始执行captureError装饰器")
try:
result = func(*args, **kwargs)
except Exception as e:
print(f"错误信息:{e}")
result = 0
print("captureError:装饰器执行完毕")
return result
return wrapper
@by
@captureError
def division(a, b):
return a/b
if __name__ == "__main__":
# 调用
result = division("2", "1")
print(f"运行结果:{result}")
# 结果
"""
captureError:进入了captureError
by:进入了by
by:开始执行装饰器
captureError:开始执行captureError装饰器
captureError:装饰器执行完毕
by:by装饰器执行完毕
运行结果:2.0
"""
执行顺序:
执行的顺序被分为了两部分,函数被装饰时,和函数调用时
函数被装饰:
当函数被装饰器所装饰的时候,执行模块就会自下而上的执行装饰器,但是此时并未调用wrapper函数1. captureError:进入了captureError
2. by:进入了by
函数调用:
当函数开始调用的时候,才开始执行wrapper函数3. by:开始执行装饰器
4. captureError:开始执行captureError装饰器
在这一步才真正的执行函数体
5. captureError:装饰器执行完毕
6. by:by装饰器执行完毕
7. 运行结果:2.0
总结:
进入装饰器:由下往上
依次进入
执行装饰器:由上往下
依次执行
附录:装饰器的实例
1. 单例模式
def singleton(cls):
instance = {}
def wrapper(*args, **kwargs):
if not instance:
instance[cls] = cls(*args, **kwargs)
return instance[cls]
return wrapper
@singleton
class MyClass(object):
pass
if __name__ == "__main__":
a = MyClass()
b = MyClass()
print(id(a)) # 4349423472
print(id(b)) # 4349423472
2. FastAPI路由
from enum import Enum
app = FastAPI()
@app.get("/items/{item_id}") # 路由装饰器
async def read_item(item_id):
return {"item_id": item_id, "type": str(type(item_id))}
源码:
def api_route(
self,
path: str,
*,
response_model: Optional[Type[Any]] = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
response_description: str = "Successful Response",
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = Default(JSONResponse),
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
) -> Callable[[DecoratedCallable], DecoratedCallable]:
def decorator(func: DecoratedCallable) -> DecoratedCallable:
self.add_api_route(
path,
func,
response_model=response_model,
status_code=status_code,
tags=tags,
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
responses=responses,
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
)
return func
return decorator
3. ddt数据驱动
@ddt
class TestLoginCases(unittest.TestCase):
@data(*login_yh.get_data('login'))
def test_login(self, case):
pass
源码:
def ddt(arg=None, **kwargs):
"""
Class decorator for subclasses of ``unittest.TestCase``.
Apply this decorator to the test case class, and then
decorate test methods with ``@data``.
For each method decorated with ``@data``, this will effectively create as
many methods as data items are passed as parameters to ``@data``.
The names of the test methods follow the pattern
``original_test_name_{ordinal}_{data}``. ``ordinal`` is the position of the
data argument, starting with 1.
For data we use a string representation of the data value converted into a
valid python identifier. If ``data.__name__`` exists, we use that instead.
For each method decorated with ``@file_data('test_data.json')``, the
decorator will try to load the test_data.json file located relative
to the python file containing the method that is decorated. It will,
for each ``test_name`` key create as many methods in the list of values
from the ``data`` key.
Decorating with the keyword argument ``testNameFormat`` can control the
format of the generated test names. For example:
- ``@ddt(testNameFormat=TestNameFormat.DEFAULT)`` will be index and values.
- ``@ddt(testNameFormat=TestNameFormat.INDEX_ONLY)`` will be index only.
- ``@ddt`` is the same as DEFAULT.
"""
fmt_test_name = kwargs.get("testNameFormat", TestNameFormat.DEFAULT)
def wrapper(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, DATA_ATTR):
for i, v in enumerate(getattr(func, DATA_ATTR)):
test_name = mk_test_name(
name,
getattr(v, "__name__", v),
i,
fmt_test_name
)
test_data_docstring = _get_test_data_docstring(func, v)
if hasattr(func, UNPACK_ATTR):
if isinstance(v, tuple) or isinstance(v, list):
add_test(
cls,
test_name,
test_data_docstring,
func,
*v
)
else:
# unpack dictionary
add_test(
cls,
test_name,
test_data_docstring,
func,
**v
)
else:
add_test(cls, test_name, test_data_docstring, func, v)
delattr(cls, name)
elif hasattr(func, FILE_ATTR):
file_attr = getattr(func, FILE_ATTR)
process_file_data(cls, name, func, file_attr)
delattr(cls, name)
return cls
# ``arg`` is the unittest's test class when decorating with ``@ddt`` while
# it is ``None`` when decorating a test class with ``@ddt(k=v)``.
return wrapper(arg) if inspect.isclass(arg) else wrapper
4. DRF自定义路由
class TestUserCasesViewSet(viewsets.ModelViewSet):
@action(methods=['post'], detail=True)
def run(self, request, *args, **kwargs):
# ...
return Response({"msg": ""}, status=200)
源码:
def action(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
"""
Mark a ViewSet method as a routable action.
`@action`-decorated functions will be endowed with a `mapping` property,
a `MethodMapper` that can be used to add additional method-based behaviors
on the routed action.
:param methods: A list of HTTP method names this action responds to.
Defaults to GET only.
:param detail: Required. Determines whether this action applies to
instance/detail requests or collection/list requests.
:param url_path: Define the URL segment for this action. Defaults to the
name of the method decorated.
:param url_name: Define the internal (`reverse`) URL name for this action.
Defaults to the name of the method decorated with underscores
replaced with dashes.
:param kwargs: Additional properties to set on the view. This can be used
to override viewset-level *_classes settings, equivalent to
how the `@renderer_classes` etc. decorators work for function-
based API views.
"""
methods = ['get'] if (methods is None) else methods
methods = [method.lower() for method in methods]
assert detail is not None, (
"@action() missing required argument: 'detail'"
)
# name and suffix are mutually exclusive
if 'name' in kwargs and 'suffix' in kwargs:
raise TypeError("`name` and `suffix` are mutually exclusive arguments.")
def decorator(func):
func.mapping = MethodMapper(func, methods)
func.detail = detail
func.url_path = url_path if url_path else func.__name__
func.url_name = url_name if url_name else func.__name__.replace('_', '-')
# These kwargs will end up being passed to `ViewSet.as_view()` within
# the router, which eventually delegates to Django's CBV `View`,
# which assigns them as instance attributes for each request.
func.kwargs = kwargs
# Set descriptive arguments for viewsets
if 'name' not in kwargs and 'suffix' not in kwargs:
func.kwargs['name'] = pretty_name(func.__name__)
func.kwargs['description'] = func.__doc__ or None
return func
return decorator