Python装饰器详解

# ------------------------------------函数装饰器和闭包------------------------------------
"""
函数装饰器用于在源码中'标记'函数,以某种方式增强函数的行为.
闭包是装饰器的基础,还是回调式异步编程和函数式编程风格的基础.
Python如何计算装饰器句法?
Python如何判断变量是不是局部的?
闭包存在的原因和工作原理?
nonlocal能解决什么问题?
"""
# ------------------------------------装饰器基础知识------------------------------------
"""
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数).装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象.
"""


def decorate(func):
    def inner(*args, **kwargs):
        ret = func(args, kwargs)
        return ret

    return inner


@decorate
def target():
    print("运行target()")


target = decorate(target)
"""
上面的这两种写法的最终结果一样:上述两个代码片段执行完毕后得到的target不一定是原来那个target函数,而是decorate(target)返回的函数.
"""


def deco(func):
    def inner():
        print("运行 inner()")

    return inner


@deco
def target():
    print("运行target()")


target()  # 运行 inner()
print(target)  # <function deco.<locals>.inner at 0x0000029C397D6950>
"""
严格来说,装饰器只是语法糖.如前所示,装饰器可用像常规的可调用对象那样调用,其参数是另一个函数.有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时.
总上,装饰器的一大特性是,能把被装饰的函数替换成其他函数.
"""

# ------------------------------------Python何时执行装饰器------------------------------------
"""
第二个特性是,装饰器在加载模块时立即执行.
"""
registry = []


def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func


@register
def f1():
    print('running f1()')


@register
def f2():
    print('running f2()')


def f3():
    print('running f3()')


def main():
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()


main()
""""
running register(<function f1 at 0x00000173D1E08598>)
running register(<function f2 at 0x00000173D1DE46A8>)
running main()
registry -> [<function f1 at 0x00000173D1E08598>, <function f2 at 0x00000173D1DE46A8>]
running f1()
running f2()
running f3()

注意,register在其它函数之前运行了两次.主要强调,函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行.
这突出了Python程序员所说的导入时和运行时之间的区别.
虽然上例中的register装饰器原封不动地返回被装饰的函数,但是这种技术并非没有用处.很多Python Web框架使用这样的装饰器把函数添加到某中央注册处,
例如把URL模式映射到生成HTTP响应的函数上的注册处.这种注册装饰器可能会也可能不会修改被装饰的函数.
"""

# ------------------------------------使用装饰器改进'策略'模式------------------------------------
"""
在'使用一等函数实现设计模式'的电商促销折扣示例中,主要的问题是,定义体中有函数的名称,但是best_promo用来判断哪个折扣幅度最大的promos列表中也有函数名称.
这种重复是个问题,因为新增策略函数后可能会忘记把它添加到promos列表中,导致best_promo忽略新策略,而且不报错,为系统引入了不易察觉的缺陷.
下面使用注册装饰器解决这个问题.
"""
promos = []


def promotion(promo_func):
    promos.append(promo_func)
    return promo_func


@promotion
def fidelity(order):
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0


@promotion
def bulk_item(order):
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount


@promotion
def large_order(order):
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0


def best_promo(order):
    return max(promo(order) for promo in promos)


"""
promotion把promo_func添加到promos列表中,然后原封不动地将其返回.
与上一个方案相比,这个方案有几个优点:
促销策略函数无需使用特殊的名称(即不用以_promo结尾).
@promotion装饰器突出了被装饰的函数的作用,还便于临时禁用某个促销策略.只需把装饰器注释掉.
促销折扣策略可用在其他模块中定义,在系统中的任何地方都行,只要使用@promotion装饰即可.

不过,多数装饰器会修改被装饰的函数.通常,它们会定义一个内部函数,然后将其返回,替换被装饰的函数.
使用内部函数的代码几乎都要靠闭包才能正确运作.
"""


# ------------------------------------变量作用域规则------------------------------------
def f1_(a):
    print(a)
    print(b)


# f1_(3)  # 3  NameError: name 'b' is not defined
b = 6
f1_(3)  # 3  6


def f2_(a):
    print(a)
    print(b)
    # b = 7


f2_(3)  # 3  UnboundLocalError: local variable 'b' referenced before assignment
"""
仔细观察上面的3个函数调用.
第一次,函数读取两个值,一个是局部变量a,是函数的参数,另一个是变量b,这个函数没有定义它.会报错.
第二次,如果先给全局变量b赋值,然后再调用f1_,那就不会出错.
第三次,首先输出了3,这表明print(a)语句执行了.但是第二个语句print(b)执行不了.一开始我很吃惊,我觉得会打印6,因为有个全局变量6,
而且是在print(b)之后为局部变量b赋值的.可事实是,Python编译函数的定义体时,它判断b是局部变量,因为在函数中给它赋值了.生成的字节码证实了这种判断,
Python会尝试从本地环境获取b.后面调用f2_(3)时,f2_的定义体会获取并打印局部变量a的值,但是尝试获取局部变量b的时候,发现b没有绑定值.

这不是缺陷,而是设计选择:Python不要求声明变量,但是假定在函数定义体赋值的变量是局部变量.这比JS的行为好多了,JS也不要求声明变量,但是如果忘记
声明变量为局部变量(使用var),可能会在不知情的情况下获取全局变量.

如果在函数中赋值时想让解释器把b当成全局变量,要使用global声明:
"""
b1 = 6


def f3_(a):
    global b1
    print(a)
    print(b1)
    b1 = 9


f3_(3)  # 3 6
print(b1)  # 9

# ------------------------------------闭包------------------------------------
"""
闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量.关键在于它能访问定义体之外的非全局变量.

假如有个名为avg的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价.每天都会增加新价格,因此平均值要考虑
至目前为止所有的价格.起初,avg是这样使用的:
"""


class Averager:
    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total / len(self.series)


avg = Averager()
print(avg(10))  # 10.0
print(avg(11))  # 10.5
print(avg(12))  # 11.0


# 使用闭包实现
def make_averager():
    series = []  # --------------------

    def averager(new_value):
        series.append(new_value)  # 闭包  series是自由变量
        total = sum(series)
        return total / len(series)  # --------------------

    return averager


avg = make_averager()
print(avg(10))  # 10.0
print(avg(11))  # 10.5
print(avg(12))  # 11.0
"""
调用make_averager时,返回一个averager函数对象.每次调用averager时,它会把参数添加到系列值中,然后计算当前平均值.

注意,这两个示例有共同之处:调用Averager()或make_averager()得到一个可调用对象avg,它会更新历史值,然后计算当前均值.
Averager类的实例avg在哪里存储历史值很明显:self.series实例属性.但是第二个示例中的avg函数在哪里寻找series呢?

注意,series是make_averager函数的局部变量,因为哪个函数定义体中初始化了series:series=[].可是,调用avg(10)时,
make_averager函数已经返回了,而它的本地作用域也一去不复返了.

在averager函数中.series是自由变量.这是一个技术术语,指未在本地作用域中绑定的变量.

审查返回的averager对象,我们发现python在__code__属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称.
"""
print(avg.__code__.co_varnames)  # ('new_value', 'total')
print(avg.__code__.co_freevars)  # ('series',)
"""
series的绑定在返回的avg函数的__closure__属性中,avg.__closure__中的各个元素对应avg.__code__.co_freevars中的一个名称.这些元素是cell对象,
有个cell_contents属性,保存着真正的值.
"""
print(avg.__closure__)  # (<cell at 0x000002DDC6931138: list object at 0x000002DDC6A79BC8>,)  series的cell对象
print(avg.__closure__[0].cell_contents)  # [10, 11, 12] series真正的值
"""
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定.这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定.
注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量.
"""

# ------------------------------------nonlocal声明------------------------------------
"""
前面实现make_averager函数的方法效率不高.主要是我们把所有值存储在历史数列中,然后在每次调用averager时使用sum求和.更好的实现方式是,
只存储目前的总值和元素个数,然后使用这两个数计算均值.
"""


# 版本1
def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        count += 1
        total += new_value
        return total / count

    return averager


"""
问题是,当count是数字或任何不可变类型时,count+=1语句的作用其实与count=count+1一样.因此,我们在averager的定义体中为count赋值了,这会把count
变成局部变量.total变量也受这个问题影响.
前面的示例没有遇到这个问题,因为我们没有给series赋值,我们只是调用series.append,并把它传给sum和len.也就是说,我们利用了列表是可变的对象这一事实.
但是对数字,字符串,元组等不可变类型来说,只能读取,不能更新.如果尝试重新绑定,例如count=count+1,其实会隐式创建局部变量count.这样,count就不是自由变量了,
因此也不会保存在闭包中.
为了解决这个问题,Python3引入了nonlocal声明.它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量.如果为nonlocal声明的变量
赋予新值,闭包中保存的绑定会更新.
"""


# 版本2
def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count

    return averager


# ------------------------------------实现一个简单的装饰器------------------------------------
"""
下面定义一个装饰器,它会在每次调用被装饰的函数时计时,然后把经过的时间,传入的参数和调用结果打印出来.
"""
import time


def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result

    return clocked


@clock
def snooze(seconds):
    time.sleep(seconds)


@clock
def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)


print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))
"""
**************************************** Calling snooze(.123)
[0.12322180s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000200s] factorial(1) -> 1
[0.00004720s] factorial(2) -> 2
[0.00008190s] factorial(3) -> 6
[0.00011090s] factorial(4) -> 24
[0.00014270s] factorial(5) -> 120
[0.00017710s] factorial(6) -> 720
6! = 720
"""

print(factorial.__name__)  # clocked
"""
现在factorial保存的是clocked函数的引用.自此之后,每次调用factorial(n),执行都是clocked(n).clocked大致做了下面几件事:
1.记录初始时间t0.
2.调用原来的factorial函数,保存结果.
3.计算经过的时间.
4.格式化收集的数据,然后打印出来.
5.返回第2步保存的结果.
这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰函数本该返回的值,同时还可以做些额外的操作.
"""

"""
上面的clock装饰器有几个缺点:
不支持关键字参数,而且遮盖了被装饰函数的__name__和__doc__属性.
使用functools.wraps装饰器把相关的属性从func复制到clocked中.此外,这个新版还能正确处理关键字参数.
"""
import functools


def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))

        arg_str = ', '.join(arg_lst)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result

    return clocked


# ------------------------------------标准库中的装饰器------------------------------------
"""
functools.wraps只是标准库中拿来即用的装饰器之一.下面介绍functools模块中最让人印象深刻的两个装饰器:

functools.lru_cache是非常实用的装饰器,它实现了备忘功能.这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算.
LRU三个字母是'Least Recently Used'的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉.
"""


@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


fibonacci(6)
"""
[0.00000050s] fibonacci(0) -> 0
[0.00000050s] fibonacci(1) -> 1
[0.00002400s] fibonacci(2) -> 1
[0.00000040s] fibonacci(1) -> 1
[0.00000040s] fibonacci(0) -> 0
[0.00000040s] fibonacci(1) -> 1
[0.00001980s] fibonacci(2) -> 1
[0.00003890s] fibonacci(3) -> 2
[0.00008120s] fibonacci(4) -> 3
[0.00000030s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00000040s] fibonacci(1) -> 1
[0.00001780s] fibonacci(2) -> 1
[0.00003470s] fibonacci(3) -> 2
[0.00000030s] fibonacci(0) -> 0
[0.00000030s] fibonacci(1) -> 1
[0.00001720s] fibonacci(2) -> 1
[0.00000040s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00000040s] fibonacci(1) -> 1
[0.00001660s] fibonacci(2) -> 1
[0.00003310s] fibonacci(3) -> 2
[0.00006640s] fibonacci(4) -> 3
[0.00011770s] fibonacci(5) -> 5
[0.00021620s] fibonacci(6) -> 8
生成第n个斐波那契数,递归方式非常耗时.浪费时间的地方很明显:
fibonacci(1)调用了8次,fibonacci(2)调用了5次...但是,如果增加两行代码,使用lru_cache,性能会显著改善.
"""


@functools.lru_cache()  # 注意,这里必须像常规函数那样调用lru_cache.
@clock  # 这里叠放了装饰器,@lru_cache()应用到@clock返回的函数上
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


fibonacci(6)
"""
[0.00000100s] fibonacci(0) -> 0
[0.00000110s] fibonacci(1) -> 1
[0.00006460s] fibonacci(2) -> 1
[0.00000280s] fibonacci(3) -> 2
[0.00011670s] fibonacci(4) -> 3
[0.00000220s] fibonacci(5) -> 5
[0.00017130s] fibonacci(6) -> 8
"""
"""
注意,因为lru_cache使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache装饰的函数,它的所有参数都必须是可散列的.
"""

# ------------------------------------单分派泛函数------------------------------------
"""
假设我们在开发一个调试Web应用的工具,我们想生成HTML,显示不同类型的Python对象.我们可能会编写这样的函数:
"""
import html


def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)


print(htmlize(str))  # <pre>&lt;class &#x27;str&#x27;&gt;</pre>
print(htmlize(list))  # <pre>&lt;class &#x27;list&#x27;&gt;</pre>
print(htmlize([1, 2, 3, 4]))  # <pre>[1, 2, 3, 4]</pre>
"""
这个函数适用于任何Python类型,但是现在我们想做个拓展,让它使用特别的方式显示某些类型:
str:把内部的换行符替换为'<br>
';不使用<pre>,而是使用<p>.
int:以十进制和十六进制显示数字.
list:输出一个HTML列表,根据各个元素的类型进行格式化.

因为Python不支持重载方法或函数,所以我们不能使用不同签名定义htmlize的变体,也无法使用不同的方式处理不同的数据类型.
在Python中,一种常见的做法是把htmlize变成一个分派函数,使用一串if/elif/elif,调用专门的函数,如htmlize_str,htmlize_int,等等.
这样不便于模块的用户拓展,还显得笨拙;时间一长,分派函数htmlize会变的很大,而且它与各个专门函数之间的耦合也很紧密.

Python3.4新增的functools.singledispatch装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数.
使用@singledispatch装饰的普通函数就会变成泛函数: 根据第一个参数类型,以不同方式执行相同的操作的一组函数.
"""
from functools import singledispatch
from collections import abc
import numbers


@singledispatch  # @singledispatch标记处理object类型的基函数
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)


@htmlize.register(str)  # 各个专门函数使用@<base_function>.register(<type>)装饰.
def _(text):  # 各个专门函数的名称无关紧要;_是个不错的选择,简单明了.
    content = html.escape(text).replace('
', '<br>
')
    return '<p>{}</p>'.format(content)


@htmlize.register(numbers.Integral)  # 为每个需要特殊处理的类型注册一个函数.numbers.Integral是int的虚拟超类.
def _(n):
    return '<pre>{0} (0x{0:x})</pre>'.format(n)


@htmlize.register(tuple)  # 可以叠放多个register装饰器,让同一个函数支持不同类型.
@htmlize.register(abc.MutableSequence)
def _(seq):
    inner = '</li>
<li>'.join(htmlize(item) for item in seq)
    return '<ul>
<li>' + inner + '</li>
</ul>'


print(htmlize({1, 2, 3}))  # <pre>{1, 2, 3}</pre>
print(htmlize(abc))  # <pre>&lt;module &#x27;collections.abc&&#x27;&gt;</pre>
print(htmlize('Heimlich & Co.
- a game'))  # <p>Heimlich &amp; Co.<br>
- a game</p>
print(htmlize(42))  # <pre>42 (0x2a)</pre>
print(htmlize(['alpha', 66, {3, 2, 1}]))
"""
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>
"""
"""
只要可能,注册的专门函数应该处理抽象基类(如numbers.Integral和abc.MutableSequence),不要处理具体实现(如int和list).
这样,代码支持的兼容类型更广泛.例如,python拓展可以子类化numbers.Integral,使用固定的位数实现int类型.
使用抽象基类检查类型,可以让代码支持这些抽象基类现有和未来的具体子类或虚拟子类.
singledispatch机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数.如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的
专门函数来处理那个类型.此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数.
"""

# ------------------------------------叠放装饰器------------------------------------
"""
装饰器是函数,因此可以组合起来使用,即可以在已经被装饰的函数上应用装饰器.
"""


@functools.lru_cache()
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


# 上面的代码等同于
fibonacci = functools.lru_cache()(clock(fibonacci))

# ------------------------------------参数化装饰器------------------------------------
"""
解析源码中的装饰器时,Python把被装饰的函数作为第一个参数传给装饰器函数.那怎么让装饰器接受其他参数呢?
答案是: 创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上.
"""
registry = []


def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func


@register
def f1():
    print('running f1()')


f1()
"""
running register(<function f1 at 0x00000111432086A8>)
running f1()
"""

# ------------------------------------一个参数化的注册装饰器------------------------------------
"""
为了让register同时具备可选的注册和注销功能,需要设置一个参数,将其设为False时,不注册被装饰的函数.
从概念上看,这个新的register函数不是装饰器,而是装饰器工厂函数.调用它会返回真正的装饰器,这才是应用到目标函数上的装饰器.
"""
registry = set()


def register(active=True):  # register接受一个可选的关键字参数
    def decorate(func):  # decorate这个内部函数是真正的装饰器;注意,它的参数是一个函数.
        print('running register(active=%s)->decorate(%s)' % (active, func))
        if active:
            registry.add(func)
        else:
            registry.discard(func)
        return func  # decorate是装饰器,必须返回一个函数

    return decorate  # register是装饰器工厂函数,因此返回decorate


@register(active=False)  # @register工厂函数必须作为函数调用,并且传入所需的参数.
def f1():
    print('running f1()')


@register()  # 即使不传入参数,@register也必须作为函数调用,返回真正的装饰器decorate
def f2():
    print('running f2()')


def f3():
    print('running f3()')


print(registry)
"""
running register(active=False)->decorate(<function f1 at 0x000002795AD0C510>)
running register(active=True)->decorate(<function f2 at 0x0000027942890D08>)
{<function f2 at 0x0000027942890D08>}
"""
"""
这里的关键是,register()要返回decorate,然后把它应用到被装饰的函数上.
注意,只有f2函数在registry中;f1不在其中,因为传给register装饰器工厂函数的参数是active=False,所以应用到f1的decorate没有把它添加到registry中.
如果不适用@句法,那就要像常规函数那样使用register;若想把f添加到registry中,则装饰f函数的句法是register()(f);
不想添加(或把它删除)的话,句法是register(active=False)(f).
"""

# ------------------------------------参数化clock装饰器------------------------------------
"""
参数化装饰器的原理相当复杂,我们刚刚讨论的那个比大多数都简单.参数会装饰器通常会把被装饰的函数替换掉,而且结构上需要多一层嵌套.
下面再次探讨clock装饰器.为它添加一个功能:让用户传入一个格式字符串,控制被装饰函数的输出.
"""
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'


def clock(fmt=DEFAULT_FMT):  # clock是参数化装饰器工厂函数
    def decorate(func):  # decorate是真正的装饰器
        def clocked(*_args):  # clocked包装被装饰的函数
            t0 = time.perf_counter()
            _result = func(*_args)  # _result是被装饰的函数返回的真正结果
            elapsed = time.perf_counter() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)  # _args是clocked的参数,args是用于显示的字符串.
            result = repr(_result)  # result是_result的字符串表示形式,用于显示.
            print(fmt.format(**locals()))
            return _result

        return clocked

    return decorate


@clock()
def snooze(seconds):
    time.sleep(seconds)


@clock('{name}:{elapsed}s')
def snooze2(seconds):
    time.sleep(seconds)


@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze3(seconds):
    time.sleep(seconds)


for i in range(3):
    snooze(.123)
"""
[0.13458910s] snooze(0.123) -> None
[0.12495400s] snooze(0.123) -> None
[0.12602300s] snooze(0.123) -> None
"""

for i in range(3):
    snooze2(.123)
"""
snooze2:0.12493909999999997s
snooze2:0.12486700000000006s
snooze2:0.12496050000000003s
"""

for i in range(3):
    snooze3(.123)
"""
snooze3(0.123) dt=0.125s
snooze3(0.123) dt=0.125s
snooze3(0.123) dt=0.125s
"""
"""
非平凡的装饰器最好通过实现__call__方法的类实现.
"""


class clock:

    def __init__(self, fmt=DEFAULT_FMT):
        self.fmt = fmt

    def __call__(self, func):
        def clocked(*_args):
            t0 = time.time()
            _result = func(*_args)
            elapsed = time.time() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)
            result = repr(_result)
            print(self.fmt.format(**locals()))
            return _result

        return clocked


@clock('{name}:{elapsed}s')
def snooze(seconds):
    time.sleep(seconds)


snooze(.123)  # snooze:0.12491536140441895s
"""
若想真正理解装饰器,需要区分导入时和运行时,还要知道变量作用域、闭包和新增的nonlocal声明.掌握闭包和nonlocal不仅对构建装饰器有帮助,
还能协助你在构建GUI程序时面向事件编程,或者使用回调处理异步I/O.
"""
原文地址:https://www.cnblogs.com/zyyhxbs/p/13235630.html