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 ?