|
5 | 5 | ----------
|
6 | 6 | 问题
|
7 | 7 | ----------
|
8 |
| -todo... |
| 8 | +你想使用一个装饰器去包装函数,但是希望返回一个可调用的实例。 |
| 9 | +你需要让你的装饰器可以同时工作在类定义的内部和外部。 |
| 10 | + |
| 11 | +| |
9 | 12 |
|
10 | 13 | ----------
|
11 | 14 | 解决方案
|
12 | 15 | ----------
|
13 |
| -todo... |
| 16 | +为了将装饰器定义成一个实例,你需要确保它实现了 ``__call__()`` 和 ``__get__()`` 方法。 |
| 17 | +例如,下面的代码定义了一个类,它在其他函数上放置一个简单的记录层: |
| 18 | + |
| 19 | +.. code-block:: python |
| 20 | +
|
| 21 | + import types |
| 22 | + from functools import wraps |
| 23 | +
|
| 24 | + class Profiled: |
| 25 | + def __init__(self, func): |
| 26 | + wraps(func)(self) |
| 27 | + self.ncalls = 0 |
| 28 | +
|
| 29 | + def __call__(self, *args, **kwargs): |
| 30 | + self.ncalls += 1 |
| 31 | + return self.__wrapped__(*args, **kwargs) |
| 32 | +
|
| 33 | + def __get__(self, instance, cls): |
| 34 | + if instance is None: |
| 35 | + return self |
| 36 | + else: |
| 37 | + return types.MethodType(self, instance) |
| 38 | +
|
| 39 | +你可以将它当做一个普通的装饰器来使用,在类里面或外面都可以: |
| 40 | + |
| 41 | +.. code-block:: python |
| 42 | +
|
| 43 | + @Profiled |
| 44 | + def add(x, y): |
| 45 | + return x + y |
| 46 | +
|
| 47 | + class Spam: |
| 48 | + @Profiled |
| 49 | + def bar(self, x): |
| 50 | + print(self, x) |
| 51 | +
|
| 52 | +在交互环境中的使用示例: |
| 53 | + |
| 54 | +.. code-block:: python |
| 55 | +
|
| 56 | + >>> add(2, 3) |
| 57 | + 5 |
| 58 | + >>> add(4, 5) |
| 59 | + 9 |
| 60 | + >>> add.ncalls |
| 61 | + 2 |
| 62 | + >>> s = Spam() |
| 63 | + >>> s.bar(1) |
| 64 | + <__main__.Spam object at 0x10069e9d0> 1 |
| 65 | + >>> s.bar(2) |
| 66 | + <__main__.Spam object at 0x10069e9d0> 2 |
| 67 | + >>> s.bar(3) |
| 68 | + <__main__.Spam object at 0x10069e9d0> 3 |
| 69 | + >>> Spam.bar.ncalls |
| 70 | + 3 |
| 71 | +
|
| 72 | +| |
14 | 73 |
|
15 | 74 | ----------
|
16 | 75 | 讨论
|
17 | 76 | ----------
|
18 |
| -todo... |
| 77 | +将装饰器定义成类通常是很简单的。但是这里还是有一些细节需要解释下,特别是当你想将它作用在实例方法上的时候。 |
| 78 | + |
| 79 | +首先,使用 ``functools.wraps()`` 函数的作用跟之前还是一样,将被包装函数的元信息复制到可调用实例中去。 |
| 80 | + |
| 81 | +其次,通常很容易会忽视上面的 ``__get__()`` 方法。如果你忽略它,保持其他代码不变再次运行, |
| 82 | +你会发现当你去调用被装饰实例方法时出现很奇怪的问题。例如: |
| 83 | + |
| 84 | +.. code-block:: python |
| 85 | +
|
| 86 | + >>> s = Spam() |
| 87 | + >>> s.bar(3) |
| 88 | + Traceback (most recent call last): |
| 89 | + ... |
| 90 | + TypeError: bar() missing 1 required positional argument: 'x' |
| 91 | +
|
| 92 | +出错原因是当方法函数在一个类中被查找时,它们的 ``__get__()`` 方法依据描述器协议被调用, |
| 93 | +在8.9小节已经讲述过描述器协议了。在这里,``__get__()`` 的目的是创建一个绑定方法对象 |
| 94 | +(最终会给这个方法传递self参数)。下面是一个例子来演示底层原理: |
| 95 | + |
| 96 | +.. code-block:: python |
| 97 | +
|
| 98 | + >>> s = Spam() |
| 99 | + >>> def grok(self, x): |
| 100 | + ... pass |
| 101 | + ... |
| 102 | + >>> grok.__get__(s, Spam) |
| 103 | + <bound method Spam.grok of <__main__.Spam object at 0x100671e90>> |
| 104 | + >>> |
| 105 | +
|
| 106 | +``__get__()`` 方法是为了确保绑定方法对象能被正确的创建。 |
| 107 | +``type.MethodType()`` 手动创建一个绑定方法来使用。只有当实例被使用的时候绑定方法才会被创建。 |
| 108 | +如果这个方法是在类上面来访问, |
| 109 | +那么 ``__get__()`` 中的instance参数会被设置成None并直接返回 ``Profiled`` 实例本身。 |
| 110 | +这样的话我们就可以提取它的 ``ncalls`` 属性了。 |
| 111 | + |
| 112 | +如果你想避免一些混乱,也可以考虑另外一个使用闭包和 ``nonlocal`` 变量实现的装饰器,这个在9.5小节有讲到。例如: |
| 113 | + |
| 114 | +.. code-block:: python |
| 115 | +
|
| 116 | + import types |
| 117 | + from functools import wraps |
| 118 | +
|
| 119 | + def profiled(func): |
| 120 | + ncalls = 0 |
| 121 | + @wraps(func) |
| 122 | + def wrapper(*args, **kwargs): |
| 123 | + nonlocal ncalls |
| 124 | + ncalls += 1 |
| 125 | + return func(*args, **kwargs) |
| 126 | + wrapper.ncalls = lambda: ncalls |
| 127 | + return wrapper |
| 128 | +
|
| 129 | + # Example |
| 130 | + @profiled |
| 131 | + def add(x, y): |
| 132 | + return x + y |
| 133 | +
|
| 134 | +这个方式跟之前的效果几乎一样,除了对于 ``ncalls`` 的访问现在是通过一个被绑定为属性的函数来实现,例如: |
| 135 | + |
| 136 | +.. code-block:: python |
| 137 | +
|
| 138 | + >>> add(2, 3) |
| 139 | + 5 |
| 140 | + >>> add(4, 5) |
| 141 | + 9 |
| 142 | + >>> add.ncalls() |
| 143 | + 2 |
| 144 | + >>> |
| 145 | +
|
0 commit comments