深入理解Python装饰器(二)

概要

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

关于装饰器的上篇地址链接
//www.greatytc.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

以上装饰器的调用顺序

  • 首先按计算优先级,不是可调用对象就地计算,所以 waste_time(200) 首先被计算,它返回一个函数对象,decorator,这个函数对象的参数设计成一个可调用对象
  • 紧接着计算 decorator(foo) , 这个函数内部返回了一个包装器 wrapper,它乐意接受任何形状的参数,然后运行 wrapper("zhangsan", 10)
  • 注意,“拆包”拆到这里 "zhangsan", 10 这对实参才传进去,此时运行耗时判断的核心逻辑
  • wrapper 一定要把 func(*args, **kwargs) 的计算结果透传回来
耗时网络调用的缓存装饰器

有一些耗时查库操作,为了减低对数据库的压力,建立缓存机制:
当有命中 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355