0

I'm trying to define a class decorator which (among other things) wraps the constructor with some custom code.

I'm using a class decorator, and not inheritance, because I want the decorator to be applicable to multiple classes in a class hierarchy, and I want the wrapped code to execute for every decorated class in the hierarchy.

I can do something like:

def decorate(klass):
    def Dec(klass):
        def __init__(self,*a,**k):
            # wrapping code here
            super().__init__(*a,**k)
            # wrapping code here
    return Dec

And it works fine for simple test cases. But, i'm afraid replacing the class by another might cause subtle breakage (for instance if a decorated class decides to do arcane stuff referencing itself). In addition, it breaks the nice default repr string of the class (it shows up as "decorate..Dec" instead of whatever klass was originally).

I tried changing the class itself:

def decorate(klass):
    old_init = klass.__init__
    def new_init(self,*a,**k):
        # wrapper code here
        old_init(self,*a,**k)
        # wrapper code here
    klass.__init__ = new_init
    return klass

This way, it maintains the proper class name and all, and it works fine as long as my constructor doesn't take any arguments. However, it breaks when applying it to, for instance, a type like str:

@decorate
class S(str):
    pass


>>> s = S('foo')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "decorate.py", line 13, in new_init
    old_init(self,*a,**k)
TypeError: object.__init__() takes no parameters

Identical code works perfectly if i'm not inheriting from str but from some dummy class.

If I override __new__ instead, it works for str but fails for a custom class:

def decorate(klass):
    old_new = klass.__new__
    def new_new(cls,*a,**k):
        # wrapper code
        i = old_init(cls,*a,**k)
        # wrapper code
        return i

    klass.__new__ = new_new

return klass


@decorate
class Foo:
    pass

@decorate
class Bar(Foo):
    def __init__(self, x):
        self.x = x

Then

>>> bar = Bar(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "decorate.py", line 12, in new_new
    i = old_init(cls,*a,**k)
  File "decorate.py", line 12, in new_new
    i = old_init(cls,*a,**k)
TypeError: object() takes no parameters

How is it possible that version changing __init__ fails ? How is it possible that by replacing a function with another one of the most generic signature type (*a,**k) and proxy the call to the original, I get a failure ?

Why does object.init seems to violate the convention of accepting at least one positional argument ?

How can I make it so it works in both scenarios ? I can't override both __init__ and __new__ to extend the same behaviour; which criterion should be used to dynamically know which one is the right one to hook ?

Should this really be implemented completely differently, for instance with a metaclass ?

b0fh
  • 1,678
  • 12
  • 28
  • 1
    Immutable types don't have an `__init__`; they use `__new__` to produce the immutable instance instead. – Martijn Pieters Dec 02 '14 at 11:24
  • The other question doesn't fully answer this one. It only shows what to do for one specific class (str), not how to write generic code that gets it right in both cases. It also doesn't explain the counter-intuitive behaviour of breaking down on a dumb generic proxying of a call; and it doesn't say whether/how this should actually be implemented with a completely different technique, like a metaclass. – b0fh Dec 02 '14 at 12:48
  • No, it didn't; it merely points out that you cannot treat mutable and immutable base classes the same here. You need to detect if `__init__` or `__new__` is used at all and base your decorating on that. – Martijn Pieters Dec 02 '14 at 12:52
  • Oh, well, okay. Is there a clean way to do that ? – b0fh Dec 02 '14 at 12:54
  • First of all, decide if you really need to support immutable types *at all*. Why do you need to subclass `str` *and* use the decorator on that subclass? Are you just trying to support everything you can imagine thrown at this thing or are you solving a specific problem? – Martijn Pieters Dec 02 '14 at 12:59
  • The decorator i'm trying to write keeps track of allocated objects in a WeakSet for debugging purposes. I don't have a specific use case that requires use of immutable objects, it's just maddening to see the abstraction break down for unforseeable reasons. – b0fh Dec 02 '14 at 13:02
  • I'd stick that in a metaclass; no need to wrap anything because the `__call__` method on the metaclass can be hooked into to track instantiating. `ClassName()` is translated to `type(ClassName).__call__`... – Martijn Pieters Dec 02 '14 at 13:12
  • Wrapping `__call__` seems to do it, thanks. If you make that an answer, i'll gladly accept it ! – b0fh Dec 02 '14 at 13:40
  • Wait, sorry, wrote too fast. Intercepting `__call__` does not work, as I want the hook to also trigger for affected superclasses. – b0fh Dec 02 '14 at 13:48
  • The metaclass is inherited; so derived classes also trigger the `__call__`. – Martijn Pieters Dec 02 '14 at 13:59
  • Yes, but I want the trigger to do something on every chosen superclass. With this approach i'd have to walk the hierarchy by hand, which is much uglier than just tagging every relevant (super)class with a decorator – b0fh Dec 02 '14 at 15:01

0 Answers0