Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example of initialsing base class in a derived class #167

Closed
BrendanSimon opened this issue Mar 17, 2017 · 8 comments
Closed

Example of initialsing base class in a derived class #167

BrendanSimon opened this issue Mar 17, 2017 · 8 comments

Comments

@BrendanSimon
Copy link

If I have a class hierarchy using attrs, how do I initialise the parent class ?

Normally the constructor would call super().__init__() right? How is this done with attrs classes?

Could you provide and example in the examples documentation please :)

@BrendanSimon
Copy link
Author

Hmmm, I found the "How Does It Work?" documentation, which states that attrs never calls super() and that it walks the class hierarchy to collect the attributes of the super classes.

Still a good example would be good :)

@Tinche
Copy link
Member

Tinche commented Mar 17, 2017

Yeah, if all your bases are attrs classes without __attrs_post_init__, you don't have to do anything. For everything else, there's __attrs_post_init__.

@BrendanSimon
Copy link
Author

How do you initialise a base class attribute with a different value?

e.g. base class has member x with default of 0 (via attr.ib), and sub-class wants to intialise x to a different value (e.g. 123).

I tried using attr.ib with default, but it seem to add an additional member (i.e. there seemed to be 2 members named x).

I did try def __attrs_post_init__(): in sub-class and set the attribute directly, and that didn't seem to create an extra member. Is that the right thing to do? Does that just create a new name? Maybe I should use super().x = 123? I presume that works - I've not used super() much.

@hynek
Copy link
Member

hynek commented Mar 20, 2017

Generally speaking, we believe that subclassing for object composition is a bad thing, therefore attrs doesn’t have explicit support for many things that it entails.

That said, getting two members x in your case looks like a bug to me and should be fixed. Would you mind posting a bug with an example? Thank you!

@hynek hynek closed this as completed May 10, 2017
@felipedau
Copy link

A while ago I was asking this same question. I was not looking for a way to overwrite an attribute, but just call super().__init__() - the attrs class I was creating was a subclass of Thread and that had to be called.

As I continued learning what I was doing, I noticed a thread was not needed anymore (and therefore the inheritance), but I'm still curious: how to initialize the parent class with attrs? It seems there are two cases:

  1. When subclassing a non-attrs class, call super().__init__() in the subclass' __attrs_post_init__()
  2. When subclassing an attrs class, call super().__attrs_post_init__() in the subclass' __attrs_post_init__()

Is that correct?

Thanks!

@hynek
Copy link
Member

hynek commented Aug 30, 2017

Looks correct, yes. Calling super().__init__() in the second case probably would work too but it would unnecessarily re-initialize the attributes it knows about.

@felipedau
Copy link

The reason I called __attrs_post_init__() in the second case was not to prevent redundancy (although that is also a good reason), but because __init__() results in a recursion until a RuntimeError is raised.

I did not mention that at first because I thought that was the wrong way to do it. I thought that super().__init__() would call the subclass' __attrs_post_init__() in the end, making it call itself.

Now that you mentioned and after thinking a second time, I think that behavior is actually wrong. super().__init__() shouldn't call the subclass' __attrs_post_init__() and in fact never calls the superclass' __attrs_post_init__(). Reproducing:

import attr


a_post_init_called = False


@attr.s
class A(object):
    a = attr.ib()

    def __attrs_post_init__(self):
        global a_post_init_called
        a_post_init_called = True


b_post_init_called = False


@attr.s
class B(A):
    b = attr.ib()

    def __attrs_post_init__(self):
        global b_post_init_called
        b_post_init_called = True

        super().__init__('')


try:
    b = B('a', 'b')
except Exception as e:
    print(e)


print('post inits called: {}, {}'.format(a_post_init_called,
                                         b_post_init_called))

output:

maximum recursion depth exceeded while calling a Python object
post inits called: False, True

(I passed '' just to show that passing self.a would not cause the problem.)

Just to clarify: I do not think that using attrs like this is correct and I am not asking you to provide anything to support code like this. I am just reporting something that seems wrong.

@Arcitec
Copy link

Arcitec commented Sep 29, 2019

To summarize: If you subclass from another attrs class, the subclass collects all attributes from the parent class hierarchy, and from itself, and generates a brand new __init__ which accepts ALL merged attributes from all classes (you can verify that with inspect.signature(thesubclass.__init__)). The validators, default values etc of the superclass are automatically understood and the new subclass __init__ that's generated does all of the work. So there is ZERO REASON to call super().__init__() (in fact it's harmful and wastes CPU since it just re-does the superclass portion of the validation/initialization, which is totally pointless since all of that work has already been done by the subclass).

But if your superclass did some special __attrs_post_init__ work, then be aware that the rules are as follows:

  • If your subclass DOES NOT HAVE its own __attrs_post_init__, then the execution is as follows: The subclass runs its own __init__ (which does the attrs work of validating and initializing variables), and then it attempts to call __attrs_post_init__ and calls the PARENT class one!
  • If your subclass DOES have its own __attrs_post_init__ (meaning there was one in both your superclass and subclass), then the execution is as follows: The subclass runs its own __init__, which then runs its own __attrs_post_init__. It will NOT automatically call the superclass post-init function. So it is YOUR JOB to call super().__attrs_post_init__()

Demonstration of all cases:

@attr.s
class A:
    a = attr.ib()
    def __attrs_post_init__(self):
            print('a hello')

@attr.s
class B(A): # Because this class has a post-init, only THIS post-init will run.
    b = attr.ib()
    def __attrs_post_init__(self):
            super().__attrs_post_init__() # Without manually calling, A's post-init wouldn't run.
            print('b hello')

@attr.s
class C(A): # Because this class lacks a post-init, the one from A will run instead.
    c = attr.ib()

Result in python console:

>>> A(1)
a hello
A(a=1)

>>> B(1,2)
a hello
b hello
B(a=1, b=2)

>>> C(1,2)
a hello
C(a=1, c=2)

Of course, in hindsight, perhaps attr should have been coded so that its __init__ function checks for super().__attrs_post_init__ and if so runs it first, and THEN runs the current class's own __attrs_post_init__. So that both of them are automatically run in the correct order.

But it's way too late to change that design now, since most people have code now that manually calls the superclass post-init function as shown in example B above.

And there are many great arguments for the way things are designed, because it means that YOU decide if you want the superclass __attrs_post_init__ to run or not. And furthermore, if __init__ had been designed to auto-run the super().__attrs_post_init__, it would probably break on multiple inheritance (in a world where C inherits from B inherits from A: the subclass C __init__ would call its immediate superclass B super().__attrs_post_init__ and then its own self.__attrs_post_init__(), but nothing would call the A __attrs_post_init__ function), so it's best that we always have to explicitly call our superclass __attrs_post_init__ manually from our own __attrs_post_init__ in all of our classes, to ensure that we fully propagate the calls throughout the class hierarchy and call all of our superclass __attrs_post_init__ functions by simply designing each class' __attrs_post_init__ that way. So I don't really dislike this design. It's fine.

I just wanted to document everything. Thanks @hynek @felipedau @BrendanSimon for all your info in this thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants