深入理解Python装饰器(二)

2021-07-22  本文已影响0人  东方胖

概要

这个续篇主要说明装饰器被解释器调用的顺序,以及一些常见的装饰器场景

关于装饰器的上篇地址链接
https://www.jianshu.com/p/f3c160bed955

2. 经典写法

[code frame1]

def deco(name=""):
    #if inspect.isroutine(name):
    #    return deco()(name)

    def decorator(func):
        func.tag1 = "abc"
        func.tag2 = "567"
        return func

    return decorator

问题
1. 为什么def decorator要包在里面?可不可以写在外面?
2. 可不可以直接对func操作,例如写成这样:
[code frame2]

def maybesimple_deco(func):
    func.tag1 = "abc"
    func.tag2 = "567"
    return func

功能上看没什么问题?都能给被修饰的函数打上两个tag,对比这两种写法,形式上第二种更简单,但是在项目中基本上比较少用,实际项目的修饰器更复杂,涉及一些条件判断,通常会对函数的修饰逻辑封装在一个内部函数里面,像[code frame 1]那样写。

类修饰

一段来自真实的项目代码:源码来自 https://github.com/robotframework/robotframework
[code frame3]

@not_keyword
def library(scope=None, version=None, doc_format=None, listener=None,
            auto_keywords=False):
    """Class decorator to control keyword discovery and other library settings.

    By default disables automatic keyword detection by setting class attribute
    ``ROBOT_AUTO_KEYWORDS = False`` to the decorated library. In that mode
    only methods decorated explicitly with the :func:`keyword` decorator become
    keywords. If that is not desired, automatic keyword discovery can be
    enabled by using ``auto_keywords=True``.

    Arguments ``scope``, ``version``, ``doc_format`` and ``listener`` set the
    library scope, version, documentation format and listener by using class
    attributes ``ROBOT_LIBRARY_SCOPE``, ``ROBOT_LIBRARY_VERSION``,
    ``ROBOT_LIBRARY_DOC_FORMAT`` and ``ROBOT_LIBRARY_LISTENER``, respectively.
    These attributes are only set if the related arguments are given and they
    override possible existing attributes in the decorated class.

    Examples::

        @library
        class KeywordDiscovery:

            @keyword
            def do_something(self):
                # ...

            def not_keyword(self):
                # ...


        @library(scope='GLOBAL', version='3.2')
        class LibraryConfiguration:
            # ...

    The ``@library`` decorator is new in Robot Framework 3.2.
    """
    if inspect.isclass(scope):
        return library()(scope)

    def decorator(cls):
        if scope is not None:
            cls.ROBOT_LIBRARY_SCOPE = scope
        if version is not None:
            cls.ROBOT_LIBRARY_VERSION = version
        if doc_format is not None:
            cls.ROBOT_LIBRARY_DOC_FORMAT = doc_format
        if listener is not None:
            cls.ROBOT_LIBRARY_LISTENER = listener
        cls.ROBOT_AUTO_KEYWORDS = auto_keywords
        return cls

    return decorator

类装饰器对比函数装饰器,几乎没有太大的分别,实际上code frame2也可以修饰类
[code frame4]

@maybesimple_deco
class FooClass(object):
    def __init__(self):
        pass

foo = FooClass()
    print(foo.tag1)

四、装饰器的代码调用顺序

4.1 多层语法糖 @@ ... 的求值顺序
@deco1(arg1)
@deco2
def myfunc()
    ...

python有关函数与类的函数执行,大体上是按照栈顺序来的,函数定义可以看成是一个入栈过程,函数实体调用,则是出栈顺序。
装饰器@语法的求值顺序是先下后上(先里后外),越靠近被修饰函数(类)的函数(定义部分)越先执行

     1  import inspect
     2
     3
     4  def deco1(ff):
     5      print("enter ff, ff's name is : %s" % ff.__name__)
     6      return ff
     7
     8
     9  def deco(name=""):
    10      print("enter deco")
    11      if inspect.isroutine(name):
    12          return deco()(name)
    13
    14      def decorator(func):
    15          func.tag1 = "abc"
    16          func.tag2 = "567"
    17          return func
    18
    19      return decorator
    20
    21
    22  class Foo(object):
    23      def __init__(self):
    24          print("enter Foo object")
    25
    26      @deco1
    27      def foo_func(self):
    28          print("enter foo_func")
    29
    30
    31  @deco1
    32  @deco("abc")
    33  def myfunc():
    34      print("enter myfunc")
    35
    36
    37  if __name__ == '__main__':
    38      myfunc()
    39
    40      f = Foo()
    41      f.foo_func()

以上代码的执行顺序(忽略空行)是
[1 4 9 22 23 26 4 5 6 26 22 31 32 9 10 11 14 19 32 14 15 16 17 32 4 5 6 32 37 38 31 34 38 40 23 24 40 26 28 41]
函数调用完成返回调用点这个特征无须赘述,比较重要的是31行~34行,这里的顺序是先检查定义,31顺序入栈到32为止,如果有多层@语法,依次类推;语法定义完毕后开始装饰器求值,此时从里向外(从下到上)依次运行函数体内的逻辑,所以32行结束后会跳到第9行开始求值,运行到14行是一个内部函数定义,忽略函数体然后运行完19行返回到32行,这时decorator继续求值,参数为myfunc,跳转到14行开始运行内部函数的定义,14~17行, 然后跳回32行,继续向上求值,运行4~6行的装饰器内部逻辑,跳转回32行... 如果有多个装饰器,直到对所有装饰器求值完毕;
对于类定义的成员函数被修饰的情形,首先类的定义行22运行,23行定义初始化函数__init__,然后26行遇到装饰器,4~6行进行装饰器求值,运行完成后回到26行,22行。
所有装饰器求值在正式的函数调用(main模块的运行)之前,所以可以理解为,装饰器的应用场景是一种想在被修饰函数(类)的实体调用之前进行操作的行为

保存为run_order.py 输出

$ python run_order.py
enter ff, ff's name is : foo_func
enter deco
enter ff, ff's name is : myfunc
enter myfunc
enter Foo object
enter foo_func

装饰器的实用场景

统计一个函数的调用耗时。这是非常经典的使用场景,怎么写这个装饰器?

我们知道,被修饰函数具有不确定的参数,所以装饰器需要包装一个变参类型。本例子说明变参函数用统一装饰器修饰的方法

代码:

def waste_time(func):
    def decorator(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        end = time.time()
        print("Spend %s %f" % (func.__name__, end - start))
        return res
    return decorator

print语句实际应用可以用异步线程做一个上报统计的逻辑代替。
使用如下:

@waste_time
def foo(name, age):
    time.sleep(1.5)

foo("Xiaoming", 15)

输出

$ Spend foo 1.502235

如果我们只统计 超过 200ms的函数应当如何,300ms呢?这就需要将超时阈值设计为一个参数,装饰器wase_time 的使用方式类似这样子:

@waste_time(200)
def foo(name, age):
    time.sleep(1.5)

装饰器应该如何设计?
首先 waste_time 的第一层参数变成了一个整型参数,被修饰函数作为参数在第二层传递,所以可在嵌套包装进去,轮廓:

def waste_time(millesecs: int):
      def wrapper(f):
            # do something
            pass
      return wrapper

这样的话 @waste_time(200) 首先调用 waste_time(200) 返回 wrapper对象,然后计算 wrapper(foo) 运行wrapper内部的逻辑。

可以看到,装饰器的设计需要抓住两点


def waste_time(millesecs: int):
    def decorator(func):
        def wrapper(*args, **kwargs):
            start = time.time()
            res = func(*args, **kwargs)
            end = time.time()
            elapse_time = (end - start) * 1000
            if elapse_time >= millesecs:
                print("Spend %s %d" % (func.__name__, elapse_time))
            return res
        return wrapper
    return decorator

搞清楚装饰器的调用顺序是按需设计装饰器的关键

@waste_time(200)
def foo(name, age):
    time.sleep(0.55)

foo("zhangsan", 100)
# 输出 Spend foo 554

以上装饰器的调用顺序

耗时网络调用的缓存装饰器

有一些耗时查库操作,为了减低对数据库的压力,建立缓存机制:
当有命中 cache 时,并且没有超时过期,直接返回上一次请求的结果,不查询db
缓存的存储结构一般是 k-v 形式,与上例有所不同,内部wrapper 的变参之需要抽出一个 key就行
装饰器可以设计成这样:

"""
secs 是缓存过期时间
"""
import time
cache_table = {} # 用内存缓存历史查询时间和结果
def cache(secs: int): 
    def decorator(func):
        def wrapper(key, *args, **kwargs):
            now = time.time()
            if key not in cache_table:
                content = func(key, *args, **kwargs)
                cache_table[key] = {"last_time": now, "content": content}
                return content
            else:
                if (now - cache_table[key]["last_time"]) < secs:
                    return cache_table[key]["content"]
                else:
                    content = func(key, *args, **kwargs)
                    cache_table[key]["last_time"] = now
                    cache_table[key]["content"] = content
                    return content
        return wrapper
    return decorator

使用如下

@cache(3000)
def get_data(key: str) ->str:
    # sql
    return content
上一篇下一篇

猜你喜欢

热点阅读