1

Not a class decorator, but a decorator class.

I can write a decorator function and it works fine, but when I try the same implemented as a class (to wrestle out of the nesting, mainly), I get caught in the selfies. Here's a minimal working example:

Note The decorator is used with parentheses like so @DecoratorClass() and not @DecoratorClass. This makes a big difference.

def decorator_function():
    def decorator(fcn):
        def wrapper(self, *args):
            print('decorator_function wrapper self:', self)
            print('decorator_function wrapper args:', args)
            return fcn(self, *args)
        return wrapper
    return decorator


class DecoratorClass:
    def __init__(self):
        self.fcn = None
    def __call__(self, fcn):
        self.fcn = fcn
        return self.wrapper
    def wrapper(self, *args):
        print('DecoratorClass wrapper self:', self)
        print('DecoratorClass wrapper args:', args)
        return self.fcn(*args)


class A:
    @decorator_function()
    def x(self, *args):
        print('A x self:', self)
        print('A x args:', args)
        return 42

    @DecoratorClass()
    def y(self, *args):
        print('A y self:', self)
        print('A y args:', args)
        return 43

a = A()
a.x(1, 2)
a.y(1, 2)

And here is the output.

decorator_function wrapper self: <mytests.debugme.A object at 0x7fa540d315f8>
decorator_function wrapper args: (1, 2)
A x self: <mytests.debugme.A object at 0x7fa540d315f8>
A x args: (1, 2)
DecoratorClass wrapper self: <mytests.debugme.DecoratorClass object at 0x7fa540d31208>
DecoratorClass wrapper args: (1, 2)
A y self: 1
A y args: (2,)

As you can see, the class based approach loses the reference to a. To clarify, I wish the result of a.y(1,2) to be identical to a.x(1,2).

update for @Martijn Pieters

class DecoratorClass:
    def __init__(self):
        self.fcn = None
    def __call__(self, fcn):
        self.fcn = fcn
        return self.wrapper
    def __get__(self, obj, objtype):
        """Support instance methods."""
        import functools
        return functools.partial(self.__call__, obj)
    def wrapper(self, *args):
        print('DecoratorClass wrapper self:', self)
        print('DecoratorClass wrapper args:', args)
        return self.fcn(*args)
a = A()
a.y(1, 2)

Output:

DecoratorClass wrapper self: <mytests.debugme.DecoratorClass object at 0x7fa540d31048>
DecoratorClass wrapper args: (1, 2)
A y self: 1
A y args: (2,)

IOW: the marked duplicate question has no working answer.

Paul
  • 766
  • 9
  • 28
  • @Martijn-Pieters Thanks for the pointer. But 1. That is about Python 2.x while I marked this Python 3.x, and 2. the accepted answer only shows how to do this with functions, which I am clearly trying to get away from. – Paul Jun 09 '17 at 21:39
  • The answers apply equally to Python 2 and 3. See the [descriptor HOWTO](https://docs.python.org/2/howto/descriptor.html); you need to make your decorator class a descriptor or return a function object (which is already a descriptor) for binding to instances to work. – Martijn Pieters Jun 09 '17 at 21:46
  • I would appreciate it if you use a proper answer post for your solution. As it stands currently, I can't translate your comments into workable code. (And I would also be able to mark as accepted when I do) – Paul Jun 09 '17 at 21:47
  • 1
    The duplicate has workable code *for you*. I merely gave you the background documentation. For example, simply adding the `__get__` method as [described in this answer](https://stackoverflow.com/a/3296318/100297) should get you there already. However, you really want to understand *why* this works, otherwise you won't be able to debug any issues you may still run into. – Martijn Pieters Jun 09 '17 at 21:49
  • [Related, if not dupe.](https://stackoverflow.com/questions/42670667/using-classes-as-method-decorators) – Aran-Fey Jun 09 '17 at 21:56
  • OK, I've added a note about a sublety that seems rather crucial. The class instance will be the decorator, not the class itself. – Paul Jun 09 '17 at 22:06
  • @MartijnPieters unless you still believe my question is "an exact duplicate of an existing question", would you be so kind to lift your curse? – Paul Jun 09 '17 at 22:08
  • 1
    @Rawing: same problem; the *instance* is being used as the decorator, not the class. So the OP has to solve another layer of indirection. Whatever `__call__` returns is the wrapper, and *that return value* must be a descriptor. `self.wrapper` is not a descriptor, it is a bound method. – Martijn Pieters Jun 09 '17 at 22:16
  • @Paul: you'll need to return a descriptor from `__call__`, `self.wrapper.__func__` could do, but then you lose your connection with the decorator instance. Why are you using a class instance to decorate? Do you really need a decorator factory? – Martijn Pieters Jun 09 '17 at 22:17
  • @MartijnPieters I will give you a link to the Github repo if you lift the dupe and post a proper answer that I can accept. – Paul Jun 09 '17 at 22:18
  • @Paul: a link to a github repo is not much use. We want questions to be self-contained *here*. The duplicate stands; the answer is that you need to return a descriptor object to replace the original method object on a class. – Martijn Pieters Jun 09 '17 at 22:21
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/146306/discussion-between-paul-and-martijn-pieters). – Paul Jun 09 '17 at 22:23

0 Answers0