Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

super not working with class decorators?

Lets define simple class decorator function, which creates subclass and adds 'Dec' to original class name only:

def decorate_class(klass):
    new_class = type(klass.__name__ + 'Dec', (klass,), {})
    return new_class

Now apply it on a simple subclass definition:

class Base(object):
    def __init__(self):
        print 'Base init'

@decorate_class
class MyClass(Base):
    def __init__(self):
        print 'MyClass init'
        super(MyClass, self).__init__()

Now, if you try instantiate decorated MyClass, it will end up in an infinite loop:

c = MyClass()
# ...
# File "test.py", line 40, in __init__
#   super(MyClass, self).__init__()
# RuntimeError: maximum recursion depth exceeded while calling a Python object

It seems, super can't handle this case and does not skip current class from inheritance chain.

The question, how correctly use class decorator on classes using super ?

Bonus question, how get final class from proxy-object created by super ? Ie. get object class from super(Base, self).__init__ expression, as determined parent class defining called __init__.

like image 507
dunrix Avatar asked Apr 27 '17 13:04

dunrix


Video Answer


2 Answers

If you just want to change the class's .__name__ attribute, make a decorator that does that.

from __future__ import print_function

def decorate_class(klass):
    klass.__name__ += 'Dec'
    return klass

class Base(object):
    def __init__(self):
        print('Base init')

@decorate_class
class MyClass(Base):
    def __init__(self):
        print('MyClass init')
        super(MyClass, self).__init__()

c = MyClass()
cls = c.__class__
print(cls, cls.__name__)

Python 2 output

MyClass init
Base init
<class '__main__.MyClassDec'> MyClassDec

Python 3 output

MyClass init
Base init
<class '__main__.MyClass'> MyClassDec

Note the difference in the repr of cls. (I'm not sure why you'd want to change a class's name though, it sounds like a recipe for confusion, but I guess it's ok for this simple example).

As others have said, an @decorator isn't intended to create a subclass. You can do it in Python 3 by using the arg-less form of super (i.e., super().__init__()). And you can make it work in both Python 3 and Python 2 by explicitly supplying the parent class rather than using super.

from __future__ import print_function

def decorate_class(klass):
    name = klass.__name__
    return type(name + 'Dec', (klass,), {})

class Base(object):
    def __init__(self):
        print('Base init')

@decorate_class
class MyClass(Base):
    def __init__(self):
        print('MyClass init')
        Base.__init__(self)

c = MyClass()
cls = c.__class__
print(cls, cls.__name__)

Python 2 & 3 output

MyClass init
Base init
<class '__main__.MyClassDec'> MyClassDec    

Finally, if we just call decorate_class using normal function syntax rather than as an @decorator we can use super.

from __future__ import print_function

def decorate_class(klass):
    name = klass.__name__
    return type(name + 'Dec', (klass,), {})

class Base(object):
    def __init__(self):
        print('Base init')

class MyClass(Base):
    def __init__(self):
        print('MyClass init')
        super(MyClass, self).__init__()

MyClassDec = decorate_class(MyClass)
c = MyClassDec()
cls = c.__class__
print(cls, cls.__name__)

The output is the same as in the last version.

like image 148
PM 2Ring Avatar answered Sep 30 '22 07:09

PM 2Ring


Since your decorator returns an entirely new class with different name, for that class MyClass object doesn't even exist. This is not the case class decorators are intended for. They are intended to add additional functionality to an existing class, not outright replacing it with some other class.

Still if you are using Python3, solution is simple -

@decorate_class
class MyClass(Base):
    def __init__(self):
        print 'MyClass init'
        super().__init__()

Otherwise, I doubt there is any straight-forward solution, you just need to change your implementation. When you are renaming the class, you need to rewrite overwrite __init__ as well with newer name.

like image 31
hspandher Avatar answered Sep 30 '22 08:09

hspandher