轻松搞定装饰器

装饰器

一、什么是装饰器

image.png
如图:由快递 ->填充物-> 快递包裹 构成了我们所收到的快递,我们的快递到我们手里,其实是经过了一层包装装饰后,变成了一个包裹,才到我们手里里,拆开包裹,原始的快递依旧完后,这就是快递包裹的作用。
在程序中,快递就是我们的原始对象,快递包裹就是我们的一个新对象,填充物就是我们的扩展功能,这一系列就构成了装饰器。
由上可得:装饰器是修改其他函数功能的函数,就是一个函数(对象)。
装饰器的意义:让其他函数在不修改任何代码的前提下增加额外功能


二、装饰器的实现原理

我们要从函数的特性->闭包->语法糖,循序渐进的了解装饰器的实现原理

2.1 函数的特性

  1. 函数作为变量传递
def add(x):
    return x+1


a = add # 把函数add当做变量赋值给a
result = a(1)
print(result)  # 2
  1. 函数作为参数传递
def add(x):
    return x+1


def excute(func):
    """

    :param func 函数名
    :return 
    """
    return func(1)


result = excute(add)  # 把add函数当做参数传递给excute函数
print(result) # 2
  1. 函数作为返回值
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
  1. 函数嵌套与跨域访问
def outer(x):
    # 函数可以嵌套函数
    def inner():
        # 嵌套函数中使用了外层函数的局部变量,属于跨域访问
        print(x)  # 10
    inner()


outer(10)

2.2 闭包

闭包就是引用了外部变量的内部函数,闭包的实现就是利用上述四个函数特性。
image.png

2.3 语法糖

简而言之,语法糖就是程序语言中提供[奇技淫巧]的一种手段和方式而已。 通过这类方式编写出来的代码,即好看又好用,好似糖一般的语法。固美其名曰:语法糖
我们通过下面的一个例子来了解使用@语法糖

import time
import random


def foo():
    time.sleep(random.randint(1, 5))
    print("I'm foo fuunction")

我们以实现一个打印上面函数运行时间(不改变原有函数)为例进行详解

  1. 函数封装
    1. 可以通过另外定义一个函数进行打印函数的运行时间
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
"""
  1. 闭包封装
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
"""
  1. 装饰器(@语法糖)
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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值