0
votes

I am writing a class with multiple constructors using @classmethod. Now I would like both the __init__ constructor as well as the classmethod constructor call some routine of the class to set initial values before doing other stuff.

From __init__ this is usually done with self:

def __init__(self, name="", revision=None):
    self._init_attributes()

def _init_attributes(self):
    self.test = "hello"

From a classmethod constructor, I would call another classmethod instead, because the instance (i.e. self) is not created until I leave the classmethod with return cls(...). Now, I can call my _init_attributes() method as

@classmethod
def from_file(cls, filename=None)
    cls._init_attributes()
    # do other stuff like reading from file
    return cls()

and this actually works (in the sense that I don't get an error and I can actually see the test attribute after executing c = Class.from_file(). However, if I understand things correctly, then this will set the attributes on the class level, not on the instance level. Hence, if I initialize an attribute with a mutable object (e.g. a list), then all instances of this class would use the same list, rather than their own instance list. Is this correct? If so, is there a way to initialize "instance" attributes in classmethods, or do I have to write the code in such a way that all the attribute initialisation is done in init?

Hmmm. Actually, while writing this: I may even have greater trouble than I thought because init will be called upon return from the classmethod, won't it? So what would be a proper way to deal with this situation?

Note: Article [1] discusses a somewhat similar problem.

1
Instead, couldn't you just make _init_attributes() a @staticmethod and explicitly pass it the object whose attributes need initializing? - martineau

1 Answers

1
votes

Yes, you'r understanding things correctly: cls._init_attributes() will set class attributes, not instance attributes.

Meanwhile, it's up to your alternate constructor to construct and return an instance. In between constructing it and returning it, that's when you can call _init_attributes(). In other words:

@classmethod
def from_file(cls, filename=None)
    obj = cls()
    obj._init_attributes()
    # do other stuff like reading from file
    return obj

However, you're right that the only obvious way to construct and return an instance is to just call cls(), which will call __init__.


But this is easy to get around: just have the alternate constructors pass some extra argument to __init__ meaning "skip the usual initialization, I'm going to do it later". For example:

def __init__(self, name="", revision=None, _skip_default_init=False):
    # blah blah

@classmethod
def from_file(cls, filename=""):
    # blah blah setup
    obj = cls(_skip_default_init=True)
    # extra initialization work
    return obj

If you want to make this less visible, you can always take **kwargs and check it inside the method body… but remember, this is Python; you can't prevent people from doing stupid things, all you can do is make it obvious that they're stupid. And the _skip_default_init should be more than enough to handle that.


If you really want to, you can override __new__ as well. Constructing an object doesn't call __init__ unless __new__ returns an instance of cls or some subclass thereof. So, you can give __new__ a flag that tells it to skip over __init__ by munging obj.__class__, then restore the __class__ yourself. This is really hacky, but could conceivably be useful.


A much cleaner solution—but for some reason even less common in Python—is to borrow the "class cluster" idea from Smalltalk/ObjC: Create a private subclass that has a different __init__ that doesn't super (or intentionally skips over its immediate base and supers from there), and then have your alternate constructor in the base class just return an instance of that subclass.


Alternatively, if the only reason you don't want to call __init__ is so you can do the exact same thing __init__ would have done… why? DRY stands for "don't repeat yourself", not "bend over backward to find ways to force yourself to repeat yourself", right?