File: 06-maintaining-decorator-state-using-a-class.md

package info (click to toggle)
python-wrapt 1.15.0-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,104 kB
  • sloc: python: 5,994; ansic: 2,354; makefile: 182; sh: 46
file content (326 lines) | stat: -rw-r--r-- 13,563 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
Maintaining decorator state using a class
=========================================

This is the sixth post in my series of blog posts about Python decorators
and how I believe they are generally poorly implemented. It follows on from
the previous post titled [Decorators which accept
arguments](05-decorators-which-accept-arguments.md), with the very first
post in the series being [How you implemented your Python decorator is
wrong](01-how-you-implemented-your-python-decorator-is-wrong.md).

In the previous post I described how to implement decorators which accept
arguments. This covered mandatory arguments, but also how to have a
decorator optionally accept arguments. I also touched on how one can
maintain state between invocations of the decorator wrapper function for a
specific wrapped function.

One of the approaches described for maintaining state was to implement the
decorator as a class. Using this approach though resulted in an unexpected
error.

This post will explore the source of the error when attempting to implement
our decorator as a class using our new decorator factory and function
wrapper, and then see if any other issues crop up.

Single decorator for functions and methods
------------------------------------------

As described in the previous post, the pattern we were trying to use so as
to allow us to use a class as a decorator was:

```python
class with_arguments(object):

    def __init__(self, arg):
        self.arg = arg

    @decorator
    def __call__(self, wrapped, instance, args, kwargs):
        return wrapped(*args, **kwargs)

@with_arguments(arg=1)
def function():
    pass
```

The intent here is that the application of the decorator, with arguments
supplied, would result in an instance of the class being created. In the
next phase where that is called with the wrapped function, the
`__call__()` method with `@decorator` applied will be used as a
decorator on the function to be wrapped. The end result should be that the
`__call__()` method of the class instance created ends up being our
wrapper function.

When the decorated function is now called, the `__call__()` method of the
class would be called with it in turn calling the wrapped function. As the
`__call__()` method at that point is bound to an instance of the class, it
would have access to the state that it contained.

When we tried this though we got, at the time that the decorator was being
applied, the error:

```pycon
Traceback (most recent call last):
  File "test.py", line 483, in <module>
    @with_arguments(1)
TypeError: _decorator() takes exactly 1 argument (2 given)
```

The `_decorator()` function in this case is the inner function from our
decorator factory.

```python
def decorator(wrapper):
    @functools.wraps(wrapper)
    def _decorator(wrapped):
        return function_wrapper(wrapped, wrapper)
    return _decorator
```

The mistake that has been made here is that we are using a function closure
to implement our decorator factory, yet we were expecting it to work on
both normal functions and methods of classes.

The reason this will not work is due to the binding that occurs when a
method of a class is accessed. This process was described in a previous
post in this series and is the result of the descriptor protocol being
applied. This binding results in the reference to the instance of the class
being automatically passed as the first argument to the method.

Now as the `_decorator()` function was acting as a wrapper for the method
call, and because `_decorator()` was not defined so as to accept both
`self` and `wrapped` as arguments, the call would fail.

We could create a special variant of the decorator factory to be used just
on instance methods, but that goes against the specific complaint expressed
earlier in regard to how people create multiple variants of decorators for
use on normal functions and instance methods.

To resolve this issue, what we can do is use our function wrapper for the
decorator returned by the decorator factory, instead of a function closure.

```python
def decorator(wrapper):
    def _wrapper(wrapped, instance, args, kwargs):
        def _execute(wrapped):
            return function_wrapper(wrapped, wrapper)
        return _execute(*args, **kwargs)
    return function_wrapper(wrapper, _wrapper)
```

Explicit binding of methods required
------------------------------------

This above change now means we do not have to worry about whether
`@decorator` is being applied to a normal function, instance method or
even a class method. This is because in all cases, any reference to the
instance being bound to is never passed through in `args`. Thus any
wrapper function doesn't need to worry about the distinction.

Trying again with this change though, we are confronted with a further
problem. This time at the point that the wrapped function is called.

```pycon
>>> function()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.py", line 243, in __call__
    return self.wrapper(self.wrapped, None, args, kwargs)
TypeError: __call__() takes exactly 5 arguments (4 given)
```

The issue this time is that when `@decorator` is applied to the
`__call__()` method, the reference it is passed is that of the unbound
method. This is because this occurs during the processing of the class
definition, long before any instance of the class has been created.

Normally the reference to the instance would be supplied later when the
method is bound, but because our decorator is actually a factory there are
two layers involved. The target instance is available to the upper factory
as the 'instance' argument, but that isn't being used in any way when the
inner function wrapper object is being created which associates the
function to be wrapped with our wrapper function.

To solve this problem we need for the case where we are being bound to an
instance, to explicitly bind the wrapper function ourselves against the
instance.

```python
def decorator(wrapper):
    def _wrapper(wrapped, instance, args, kwargs):
        def _execute(wrapped):
            if instance is None:
                return function_wrapper(wrapped, wrapper)
            elif inspect.isclass(instance):
                return function_wrapper(wrapped, wrapper.__get__(None, instance))
            else:
                return function_wrapper(wrapped, wrapper.__get__(instance, type(instance)))
        return _execute(*args, **kwargs)
    return function_wrapper(wrapper, _wrapper)
```

So what we are using here is the feature of our function wrapper that
allows us to implement a universal decorator. That is, one which can change
its behaviour dependent upon the context it is used in.

In this case we had three cases we needed to deal with.

The first is where the instance was `None`. This corresponds to a normal
function, a static method or where the decorator was applied to a class
type.

The second is where the instance was not `None`, but where it referred to a
class type. This corresponds to a class method. In this case we need to
bind the wrapper function to the class type by calling the `__get__()`
method of the wrapper function explicitly.

The third and final case is where the instance was not `None`, but where
it was not referring to a class type. This corresponds to an instance
method. In this case we again need to bind the wrapper function, this time
to the instance.

With these changes, we are now all done with addressing this issue, and to
a large degree with filling out our new decorator pattern.

Do not try and reproduce this
-----------------------------

So the complete solution we now have at this point is:

```python
class object_proxy(object):

    def __init__(self, wrapped):
        self.wrapped = wrapped
        try:
            self.__name__ = wrapped.__name__
        except AttributeError:
            pass

    @property
    def __class__(self):
        return self.wrapped.__class__

    def __getattr__(self, name):
        return getattr(self.wrapped, name)

class bound_function_wrapper(object_proxy):

    def __init__(self, wrapped, instance, wrapper, binding, parent):
        super(bound_function_wrapper, self).__init__(wrapped)
        self.instance = instance
        self.wrapper = wrapper
        self.binding = binding
        self.parent = parent

    def __call__(self, *args, **kwargs):
        if self.binding == 'function':
            if self.instance is None:
                instance, args = args[0], args[1:]
                wrapped = functools.partial(self.wrapped, instance)
                return self.wrapper(wrapped, instance, args, kwargs)
            else:
                return self.wrapper(self.wrapped, self.instance, args, kwargs)
        else:
            instance = getattr(self.wrapped, '__self__', None)
            return self.wrapper(self.wrapped, instance, args, kwargs)

    def __get__(self, instance, owner):
        if self.instance is None and self.binding == 'function':
            descriptor = self.parent.wrapped.__get__(instance, owner)
            return bound_function_wrapper(descriptor, instance, self.wrapper,
                    self.binding, self.parent)
        return self

class function_wrapper(object_proxy):

    def __init__(self, wrapped, wrapper):
        super(function_wrapper, self).__init__(wrapped)
        self.wrapper = wrapper
        if isinstance(wrapped, classmethod):
            self.binding = 'classmethod'
        elif isinstance(wrapped, staticmethod):
            self.binding = 'staticmethod'
        else:
            self.binding = 'function'

    def __get__(self, instance, owner):
        wrapped = self.wrapped.__get__(instance, owner)
        return bound_function_wrapper(wrapped, instance, self.wrapper,
                self.binding, self)

    def __call__(self, *args, **kwargs):
        return self.wrapper(self.wrapped, None, args, kwargs)

def decorator(wrapper):
    def _wrapper(wrapped, instance, args, kwargs):
        def _execute(wrapped):
            if instance is None:
                return function_wrapper(wrapped, wrapper)
            elif inspect.isclass(instance):
                return function_wrapper(wrapped, wrapper.__get__(None, instance))
            else:
                return function_wrapper(wrapped, wrapper.__get__(instance, type(instance)))
        return _execute(*args, **kwargs)
    return function_wrapper(wrapper, _wrapper)
```

Take heed though of what was said in prior posts though. The object proxy
implementation given here is not a complete solution. As a result, do not
take this code and try and use it yourself as is. If you do you will find
that some aspects of performing introspection on the wrapped function will
not work as indicated they should.

In particular, access to the function `__doc__` string will always yield
`None`. Various attributes such as `__qualname__` in Python 3 and
`__module__` are not propagated either.

Handling an attribute such as `__doc__` string correctly is actually a
bit of a pain. This is because you cannot use a `__doc__` property in an
object proxy base class that returns the value from the wrapped function
and have it then work when you derive another class from it. This is
because the separate `__doc__` string attribute from the derived class,
even if no documentation string were specified in the derived class, will
override that of the base class.

So the object proxy as shown here was intended to be illustrative only of
what was required.

In some respects all the code here is meant to be illustrative only. It is
not here to say use this code but to show you the general path to
implementing a more robust decorator implementation. It is to provide a
narrative from which you can learn. If you were expecting a one line TLDR
summary on how to do it, then you can forget it, things just aren't that
simple.

Introducing the wrapt decorator module
--------------------------------------

If I am telling you not to use this code, what are you supposed to do then?

The answer to that already exists in the form of the wrapt module on PyPi.

The wrapt package has been available for a number of months already, but
isn't widely known of at this point. It implements all of what is described
but also more. The module has a complete implementation of the object proxy
required to make all this work correctly. The module also provides a range
of other features related to the decorator factory as well as other
separate features related to monkey patching in general.

Although I am now finally pointing out that this module exists, I will not
be stopping the blog posts at this point as there is a range of topics I
still want to cover. These include examples of how a universal decorator
can be used, enabling/disabling of decorators, performance issues, the
remaining parts of the implementation of the object proxy, monkey patching
and much more.

In the next post in this series I will look at one specific example of
using a universal decorator by posing the question of if Python decorators
are so wonderful, why does Python not provide a `@synchronized` decorator?

Such a decorator was held up as a bit of a poster child as to what could be
done with decorators when they were first introduced to the language, yet
all the implementations I could find are half baked and not very practical
in the real world. I believe that a universal decorator can help here and
we can actually have a usable `@synchronized` decorator. I will therefore
explore that possibility in the next post.