0

problem

I tried to call the constructor of a class inside another class' constructor's parameter's default assignment, but I encountered this problem where the constructor isn't called correctly. What is happening here?

code explanation

The Bad class is the case that doesn't work and the Good class is an ugly work around to solve the problem. The value Base.i is incremented for every time the constructor Base.__init__ is called, and as can be seen, it isn't incremented correctly for the o2 object, but it seems to be incremented correctly for each of o1, o3 and o4.

code

class Base:
    i = 0
    def __init__(self):
        Base.i += 1
        self.i = Base.i

class Bad:
    def __init__(self, base = Base()):
        self.base = base

class Good:
    def __init__(self, base = None):
        if base is None:
            self.base = Base()
        else:
            self.base = base

if __name__ == "__main__":
    o1 = Bad()
    o2 = Bad()

    print("Bad:")
    print(f"o1.i: {o1.base.i}")
    print(f"o2.i: {o2.base.i}")

    o3 = Good()
    o4 = Good()

    print("Good:")
    print(f"o3.i: {o3.base.i}")
    print(f"o4.i: {o4.base.i}")

output

o1.i: 1
o2.i: 1
Good:
o3.i: 2
o4.i: 3
dekuShrub
  • 466
  • 4
  • 20
  • 1
    When checking for None, use: if base is None as described here: https://stackoverflow.com/questions/3965104/not-none-test-in-python – Simon92 Mar 18 '21 at 11:00

1 Answers1

5

Default arguments are evaluated at definition time, so here

class Bad:
    def __init__(self, base = Base()):
        self.base = base

the name base (and hence self.base) is assigned an instance of Base at the time of definition already.

One way is to use a default of None, as you already do in class Good

class Good:
    def __init__(self, base = None):
        if base is None:
            self.base = Base()
        else:
            self.base = base

You could also define the __init__ of Bad like this:

class Bad:
    def __init__(self, base = Base):
        self.base = base()

which will assign an instance of Base when Bad is initialized, not at definition time, so each instance of Bad will have its own Base.

If you want to able to pass arguments to base, you'd have to add options

class Bad:
    def __init__(self, base = Base, base_args=(,), base_kwargs={}):
        self.base = base(*base_args, **base_kwargs)

but in that case the None way is simpler to use, since you can just pass the constructed instance directly.

Jan Christoph Terasa
  • 5,781
  • 24
  • 34
  • That `self.base = base()` doesn't allow custom arguments like `Bad(Base())`. – dekuShrub Mar 18 '21 at 11:08
  • 1
    Yes, in that case the `None` way if the way to go. I just wanted to show a solution in case all your passed classes have the same argument-less signature (unlikely). I think I have seen the `None` way of solving this quite often, so it's not a bad idea. – Jan Christoph Terasa Mar 18 '21 at 11:10
  • That's not what I'm doing. I'm trying to pass an argument to the `Bad` constructor. The `Good` class in my question allows it, but the solution with `self.base = base()` does not. – dekuShrub Mar 18 '21 at 11:14
  • That's what I just said, in case you want to do that you have to use the `None` way. The latter only works if all your constructors take no arguments. Basically you already found the way to solve this yourself. – Jan Christoph Terasa Mar 18 '21 at 11:16
  • Ah I see, I must have misread your comment. I will accept this answer if nothing else comes up. – dekuShrub Mar 18 '21 at 12:44