装饰器基础知识

装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。

假如有个名为 decorate 的装饰器:

@decorate
def target():
    pprint('running target()')

上述代码的效果与下述写法一样:

def target():
    print('running target()')
    
target = decorate(target)

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

举个🌰 装饰器通常把函数替换成另一个函数

1 def deco(func):
 2     def inner():
 3         print('running in inner()')
 4     return inner
 5 
 6 @deco       
 7 def target():
 8     print('running in target()')
 9 
10 target()

以上代码执行的结果为:

running in

  严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。

装饰器在加载模块时立即执行。 

Python何时执行装饰器

  装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时),如 🌰 的registration.py 模块所示。

1 registry = []
 2 
 3 def register(func):
 4     '''
 5     :param func:被装饰器的函数
 6     :return: 返回被装饰的函数func
 7     '''
 8     print('running register(%s)' % func)        #获取形参func的的引用
 9     registry.append(func)                       #获取的引用地址放入到类表中
10     return func                                 #执行装饰器的时候返回func
11 
12 @register
13 def f1():
14     print('running f1()')
15 
16 @register
17 def f2():
18     print('running f2()')
19 
20 def f3():
21     print('running f3()')
22 
23 def main():
24     print('running main()')
25     print('registry ->', registry)
26     f1()
27     f2()
28     f3()
29 
30 if __name__ == "__main__":
31     main()

以上代码执行的结果为:

running register(<function f1 at 0x101c7bf28>)
running register(<function f2 at 0x101c83048>)
running main()
registry -> [<function f1 at 0x101c7bf28>, <function f2 at 0x101c83048>]
running f1()
running f2()
running f3()

注意,register 在模块中其他函数之前运行(两次)。调用register 时,传给它的参数是被装饰的函数,例如 <function f1 at 0x101c7bf28>。

加载模块后,registry 中有两个被装饰函数的引用:f1 和 f2。这两个函数,以及 f3,只在 main 明确调用它们时才执行。

 

使用装饰器改进“策略”模式

   使用注册装饰器可以改进的电商促销折扣 🌰 回顾一下,函数策略主要问题是,定义体中有函数的名称,但是best_promo 用来判断哪个折扣幅度最大的 promos 列表中也有函数名称。这种重复是个问题,因为新增策略函数后可能会忘记把它添加到promos 列表中,导致 best_promo 忽略新策略,而且不报错,为系统引入了不易察觉的缺陷。下面的 🌰 使用注册装饰器解决了这个问题。

🌰 promos 列表中的值使用 promotion 装饰器填充

1 promos = []
 2 
 3 def promotion(promo_func):
 4     promos.append(promo_func)
 5     return promo_func
 6 
 7 @promotion
 8 def fidelity(order):
 9     """为积分为1000或以上的顾客提供5%折扣"""
10     return order.total() * .05 if order.customer.fidelity >= 1000 else 0
11 
12 @promotion
13 def bulk_item(order):
14     """单个商品为20个或以上时提供10%折扣"""
15     discount = 0
16     for item in order.cart:
17         if item.quantity >= 20:
18             discount += item.total() * .1
19     return discount
20 
21 @promotion
22 def large_order(order):
23     """订单中的不同商品达到10个或以上时提供7%折扣"""
24     distinct_items = {item.product for item in order.cart}
25     if len(distinct_items) >= 10:
26         return order.total() * .07
27     return 0
28 
29 def best_promo(order):
30     """选择可用的最佳折扣"""
31     return max(promo(order) for promo in promos)

与函数策略给出的方案相比,这个方案有几个优点:

  1. @promotion装饰器突出了被装饰的函数的作用,还便于临时禁用某个促销策略,只需要把装饰器注释掉
  2. 促销折扣策略可以在其他的模块中定义,在系统中的任何地方都行,只要使用@promotion装即可

 

变量作用域规则

  举个 🌰  来说明函数作用域的问题,我们定义并测试一个函数,它读取两个两个变量的值:一个是局部变量a,是函数的参数;另外一个变量b,这个函数没有定义它!

>>> def f1(a):
...     print(a)
...     print(b)
... 
>>> f1(3)
3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f1
NameError: name 'b' is not defined

如果先给全局变量 b 赋值,然后再调用 f,那就不会出错:

>>> b = 10
>>> def f1(a):
...     print(a)
...     print(b)
... 
>>> f1(50)
50
10

来个让你大吃一惊的 🌰  在函数的内部直接修改全局变量b,我们看下会出现什么问题

>>> b = 10
>>> def f1(a):
...     print('f1 a:', a)
...     print('f1 b:', b)
...     b = 3
... 
>>> f1(3)
f1 a: 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f1
UnboundLocalError: local variable 'b' referenced before assignment

  注意,首先输出了 3,这表明 print(a) 语句执行了。但是第二个语句print(b) 执行不了。一开始我很吃惊,我觉得会打印 10,因为有个全局变量 b,而且是在 print(b) 之后为局部变量 b 赋值的。

  可事实是,Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,Python 会尝试从本地环境获取 b。后面调用 f1(3) 时, f1 的定义体会获取并打印局部变量 a 的值,但是尝试获取局部变量 b 的值时,发现 b 没有绑定值。

  如何解决上面的问题呢,如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声明:

>>> b = 10
>>> def f1(a):
...     global b
...     print('f1 a:', a)
...     print('f1 b:', b)
...     b = 20
...     print('f1 更改b值以后:', b)
... 
>>> print('全局的b值', b)
全局的b值 10
>>> f1(3)
f1 a: 3
f1 b: 10
f1 更改b值以后: 20
>>> print(b)
20

 闭包

  闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。

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

🌰 Low B版本的写法~

1 class Averager():
 2 
 3     def __init__(self):
 4         self.series = []
 5 
 6     def __call__(self, new_value):          #提供实例化以后可以直接通过传递值直接调用
 7         self.series.append(new_value)
 8         total = sum(self.series)
 9         return total/len(self.series)
10 
11 avg = Averager()                 
12 print(avg(10))
13 print(avg(11))
14 print(avg(12))

以上代码执行的结果为:

10.0
10.5
11.0

函数式实现的方式:

1 def make_averager():
 2     series = []
 3 
 4     def avgerager(new_value):
 5         series.append(new_value)
 6         total = sum(series)
 7         return total/len(series)
 8 
 9     return avgerager
10 
11 avg = make_averager()               #返回内部的avgerager函数
12 print(avg(10))                      
13 print(avg(11))
14 print(avg(12))

  调用 make_averager 时,返回一个 averager 函数对象。每次调用averager 时,它会把参数添加到系列值中,然后计算当前平均值。

在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量,参见下图

闭包函数是什么python python函数闭包的应用_闭包函数是什么python

 

averager 的闭包延伸到那个函数的作用域之外,包含自由变量 series 的绑定

闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。

 

nonlocal声明

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

🌰 计算移动平均值的高阶函数,不保存所有历史值,但有缺陷

1 def make_averager():
 2     count = 0
 3     total = 0
 4 
 5     def averager(new_value):
 6         count += 1
 7         total += new_value
 8         return total / count
 9 
10     return averager
11 
12 avg = make_averager()
13 avg(10)

上面的 🌰 报错了~,因为啥呢,往下面看

Traceback (most recent call last):
.........
.........        
    count += 1
UnboundLocalError: local variable 'count' referenced before assignment

  问题是,当 count 是数字或任何不可变类型时,count += 1 语句的作用其实与 count = count + 1 一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。total 变量也受这个问题影响。

为了解决这个问题,Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。最新版 make_averager 的正确实现如示例:

🌰  计算移动平均值,不保存所有历史(使用 nonlocal 修正)

1 def make_averager():
 2     count = 0
 3     total = 0
 4 
 5     def averager(new_value):
 6         nonlocal  count, total      #类似于之前的global的用法,对于闭包的函数内部变量来说,修外层的函数作用域引发的问题,通过nonlocal来解决
 7         count += 1
 8         total += new_value
 9         return total / count
10 
11     return averager
12 
13 avg = make_averager()
14 print(avg(10))
15 print(avg(11))
16 print(avg(12))

以上代码直接的结果为:

10.0
10.5
11.0

 

实现一个简单的装饰器

定义了一个装饰器,它会在每次调用被装饰的函数时计时,然后把经过的时间、传入的参数和调用的结果打印出来。

🌰  一个简单的装饰器,输出函数的运行时间

1 import time
 2 from functools import wraps
 3 
 4 
 5 def clock(func):
 6     @wraps(func)
 7     def clocked(*args):
 8         t0 = time.perf_counter()            #启动时间
 9         result = func(*args)                #调用被装饰的函数
10         elapsed = time.perf_counter() - t0  #获取被调用函数花费了多久
11         name = func.__name__                #获取被调用函数的函数名
12         arg_str = ', '.join(repr(arg) for arg in args)  #格式化字符串拼接
13         print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
14         return result                       #返回被装饰函数运行的结果
15 
16     return clocked                          #返回内部的装饰器函数
17 
18 @clock                                      #等同于snooze = clock(snooze)
19 def snooze(seconds):
20     time.sleep(seconds)
21 
22 @clock
23 def factorial(n):
24     return 1 if n < 2 else n*factorial(n-1)
25 
26 
27 if __name__ == '__main__':
28     print('*' * 40, 'Calling snooze(.123)')
29     snooze(.123)
30     print('*' * 40, 'Calling factorial(6)')
31     print('6! =', factorial(6))

以上代码执行的结果为:

**************************************** Calling snooze(.123)
[0.12482605s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000110s] factorial(1) -> 1
[0.00002436s] factorial(2) -> 2
[0.00003814s] factorial(3) -> 6
[0.00004967s] factorial(4) -> 24
[0.00006173s] factorial(5) -> 120
[0.00007719s] factorial(6) -> 720
6! = 720

工作原理:

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

等同于:

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

factorial = clock(factorial)

  因此,在两个示例中,factorial 会作为 func 参数传给 clock。然后, clock 函数会返回 clocked 函数,Python 解释器在背后会把 clocked 赋值给 factorial。其实,导入clockdeco_demo 模块后查看 factorial 的 __name__ 属性,会得到如下结果:

>>> import clockdeco_demo
>>> clockdeco_demo.factorial.__name__
'clocked'
>>>

  所以,现在 factorial 保存的是 clocked 函数的引用。自此之后,每次调用 factorial(n),执行的都是 clocked(n)。clocked 大致做了下面几件事。

  1. 记录初始化时间t0
  2. 调用原来的factorial函数,保存结果
  3. 计算经过的时间
  4. 格式化手机的数据,然后打印出来
  5. 返回第2步保存的结果

这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作。

  下面的 🌰 使用functools.wraps 装饰器把相关的属性从 func 复制到 clocked 中。此外,这个新版还能正确处理关键字参数。

1 def clock(func):
 2     @wraps(func)
 3     def clocked(*args, **kwargs):
 4         t0 = time.time()
 5         result = func(*args, **kwargs)
 6         elapsed = time.time() - t0
 7         name = func.__name__
 8         arg_list = []
 9         if args:
10             arg_list.append(', '.join(repr(arg) for arg in args))
11         if kwargs:
12             pairs = ['%s=%s' % (k, v) for k, v in sorted(kwargs.items())]
13             arg_list.append(','.join(pairs))
14         arg_str = ','.join(arg_list)
15         print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
16         return result
17 
18     return clocked

 

标准库中的装饰器

  Python 内置了三个用于装饰方法的函数:property、classmethod 和staticmethod。另一个常见的装饰器是 functools.wraps,它的作用是协助构建行为良好的装饰器。标准库中最值得关注的两个装饰器是 lru_cache 和全新的 singledispatch(Python 3.4 新增)。这两个装饰器都在 functools 模块中定义。接下来分别讨论它们。

使用functools.lru_cache做备忘

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

生成第 n 个斐波纳契数这种慢速递归函数适合使用 lru_cache

举个🌰  生成第 n 个斐波纳契数,递归方式非常耗时

1 from functools import wraps
 2 import time
 3 
 4 
 5 def clock(func):
 6     @wraps(func)
 7     def clocked(*args, **kwargs):
 8         t0 = time.time()
 9         result = func(*args, **kwargs)
10         elapsed = time.time() - t0
11         name = func.__name__
12         arg_list = []
13         if args:
14             arg_list.append(', '.join(repr(arg) for arg in args))
15         if kwargs:
16             pairs = ['%s=%s' % (k, v) for k, v in sorted(kwargs.items())]
17             arg_list.append(','.join(pairs))
18         arg_str = ','.join(arg_list)
19         print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
20         return result
21 
22     return clocked
23 
24 @clock
25 def fibonacci(n):
26     if n < 2:
27         return n
28     return fibonacci(n-1) + fibonacci(n-2)
29 
30 if __name__ == "__main__":
31     print(fibonacci(6))

以上代码执行的结果为:

[0.00000000s] fibonacci(1) -> 1 
[0.00000095s] fibonacci(0) -> 0 
[0.00006294s] fibonacci(2) -> 1 
[0.00000000s] fibonacci(1) -> 1 
[0.00007701s] fibonacci(3) -> 2 
[0.00000000s] fibonacci(1) -> 1 
[0.00000000s] fibonacci(0) -> 0 
[0.00001192s] fibonacci(2) -> 1 
[0.00010014s] fibonacci(4) -> 3 
[0.00000095s] fibonacci(1) -> 1 
[0.00000095s] fibonacci(0) -> 0 
[0.00001192s] fibonacci(2) -> 1 
[0.00000000s] fibonacci(1) -> 1 
[0.00002360s] fibonacci(3) -> 2 
[0.00013614s] fibonacci(5) -> 5 
[0.00000000s] fibonacci(1) -> 1 
[0.00000381s] fibonacci(0) -> 0 
[0.00059795s] fibonacci(2) -> 1 
[0.00000191s] fibonacci(1) -> 1 
[0.00063992s] fibonacci(3) -> 2 
[0.00000095s] fibonacci(1) -> 1 
[0.00000119s] fibonacci(0) -> 0 
[0.00004625s] fibonacci(2) -> 1 
[0.00071788s] fibonacci(4) -> 3 
[0.00087523s] fibonacci(6) -> 8 
8

  浪费时间的地方很明显:fibonacci(1) 调用了 8 次,fibonacci(2)调用了 5 次……但是,如果增加两行代码,使用 lru_cache,性能会显著改善。

举个🌰 使用缓存实现,速度更快

1 from functools import wraps, lru_cache
 2 import time
 3 
 4 
 5 def clock(func):
 6     @wraps(func)
 7     def clocked(*args, **kwargs):
 8         t0 = time.time()
 9         result = func(*args, **kwargs)
10         elapsed = time.time() - t0
11         name = func.__name__
12         arg_list = []
13         if args:
14             arg_list.append(', '.join(repr(arg) for arg in args))
15         if kwargs:
16             pairs = ['%s=%s' % (k, v) for k, v in sorted(kwargs.items())]
17             arg_list.append(','.join(pairs))
18         arg_str = ','.join(arg_list)
19         print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
20         return result
21 
22     return clocked
23 
24 @lru_cache()
25 @clock
26 def fibonacci(n):
27     if n < 2:
28         return n
29     return fibonacci(n-1) + fibonacci(n-2)
30 
31 
32 if __name__ == "__main__":
33     print(fibonacci(6))

以上代码执行的结果为:

[0.00000095s] fibonacci(1) -> 1 
[0.00000072s] fibonacci(0) -> 0 
[0.00007081s] fibonacci(2) -> 1 
[0.00008035s] fibonacci(3) -> 2 
[0.00008821s] fibonacci(4) -> 3 
[0.00009608s] fibonacci(5) -> 5 
[0.00010514s] fibonacci(6) -> 8 
8

特别要注意,lru_cache 可以使用两个可选的参数来配置。它的签名是:

functools.lru_cache(maxsize=128, typed=False)

  maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为 2 的幂。typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。顺便说一下,因为 lru_cache 使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的。

单分派泛函数

假设我们在开发一个调试 Web 应用的工具,我们想生成 HTML,显示不同类型的 Python 对象

我们可能会编写这样的函数:

1 from functools import singledispatch
 2 from collections import abc
 3 import numbers
 4 import html
 5 
 6 
 7 @singledispatch
 8 def htmlize(obj):
 9     content = html.escape(repr(obj))
10     return '<pre>{}</pre>'.format(content)
11 
12 @htmlize.register(str)
13 def _(text):
14     content = html.escape(text).replace('\n', '<br>\n')
15     return '<p>{0}</p>'.format(content)
16 
17 @htmlize.register(numbers.Integral)
18 def _(n):
19     return '<pre>{0} (0x{0:x})</pre>'.format(n)
20 
21 @htmlize.register(tuple)
22 @htmlize.register(abc.MutableSequence)
23 def _(seq):
24     inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
25     return '<ul>\n<li>' + inner + '</li>\n</ul>'
26 
27 
28 print(htmlize({1, 2, 3}))
29 print('-'*50)
30 print(htmlize(abs))
31 print('-'*50)
32 print(htmlize('Heimlich & Co.\n- a game'))
33 print('-'*50)
34 print(htmlize(['alpha', 66, {3, 2, 1}]))

以上代码执行的结果为:

<pre>{1, 2, 3}</pre>
--------------------------------------------------
<pre><built-in function abs></pre>
--------------------------------------------------
<p>Heimlich & Co.<br>
- a game</p>
--------------------------------------------------
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>

 叠放装饰器

把 @d1 和 @d2 两个装饰器按顺序应用到 f 函数上,作用相当于 f =d1(d2(f))。

@d1
@d2
def f():
    print('f')

等同于

def f():
    print('f')

f = d1(d2(f))

 

参数化装饰器

  解析源码中的装饰器时,Python 把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。不明白什么意思?当然。下面以我们见过的最简单的装饰器为例说明:

  registration.py 模块的删减版,这里再次给出是为了便于讲解

1 registry = []
 2 
 3 def register(func):
 4     print('running register(%s)' % func)
 5     registry.append(func)
 6     return func
 7 
 8 @register
 9 def f1():
10     print('running f1()')
11 
12 print('running main()')
13 print('registry ->', registry)
14 f1()

  为了便于启用或禁用 register 执行的函数注册功能,我们为它提供一个可选的 active 参数,设为 False 时,不注册被装饰的函数。实现方式参见下面 🌰 。从概念上看,这个新的 register 函数不是装饰器,而是装饰器工厂函数。调用它会返回真正的装饰器,这才是应用到目标函数上的装饰器。

为了接受参数,新的 register 装饰器必须作为函数调用

1 registry = set()                   #创建一个空集合,用存放删除函数
 2 def register(active=True):
 3     def decorate(func):            #真正的装饰器
 4         print('running register(active=%s)->decorate(%s)' % (active, func))
 5         if active:                 #注册的为真
 6             registry.add(func)     #把被装饰的函数添加到集合中
 7         else:
 8             registry.discard(func) #删除集合中的函数
 9         return func                #返回被装饰的函数
10 
11     return decorate
12 
13 @register(active=False)
14 def f1():
15     print('running f1()')
16 
17 @register()
18 def f2():
19     print('running f2()')
20 
21 def f3():
22     print('running f3()')
23 
24 def main():
25     f1()
26     f2()
27     f3()
28 
29 main()

以上代码执行的结果为:

running register(active=False)->decorate(<function f1 at 0x10147bf28>)
running register(active=True)->decorate(<function f2 at 0x101483048>)
running f1()
running f2()
running f3()

工作原理:

@register(active=False)
def f1():
    print('running f1()')

等同于:

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

f1 = register(active=True)(f1)

 这里的关键是,register() 要返回 decorate,然后把它应用到被装饰的函数上。

参数化clock装饰器

  clock 装饰器,为它添加一个功能:让用户传入一个格式字符串,控制被装饰函数的输出

1 from functools import wraps
 2 import time
 3 
 4 DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
 5 
 6 
 7 def clock(fmt=DEFAULT_FMT):
 8     def decorate(func):
 9         @wraps(func)
10         def clocked(*_args):
11             t0 = time.time()
12             _result = func(*_args)
13             elapsed = time.time() - t0
14             name = func.__name__
15             args = ', '.join(repr(arg) for arg in _args)
16             result = repr(_result)
17             print(fmt.format(**locals()))
18             return _result
19         return clocked
20     return decorate
21 
22 
23 if __name__ == "__main__":
24     @clock()
25     def snooze(seconds):
26         time.sleep(seconds)
27 
28     @clock('{name}: {elapsed}s')
29     def snooze1(seconds):
30         time.sleep(seconds)
31 
32     @clock('{name}({args}) dt={elapsed:0.3f}s')
33     def snooze2(seconds):
34         time.sleep(seconds)
35 
36     pool = [snooze, snooze1, snooze2]
37 
38     for obj in pool:
39         for i in range(3):
40            obj(.123)

以上代码执行的结果为:

[0.12584805s] snooze(0.123) -> None
[0.12616611s] snooze(0.123) -> None
[0.12748885s] snooze(0.123) -> None
snooze1: 0.12738299369812012s
snooze1: 0.127885103225708s
snooze1: 0.12749981880187988s
snooze2(0.123) dt=0.128s
snooze2(0.123) dt=0.127s
snooze2(0.123) dt=0.127s