I don't think any of the answers here get it quite right, although some come close.
Many answers suggest something like the following:
- Provide a "most general"
__init__
function, which takes all possible arguments
__init__
should (in general) have some complex logic to check all the arguments for consistency, and then set member data depending on those arguments
- Other "constructor functions" should have more specific combinations of arguments, and all of them should call
__init__
I think this is the wrong design. Unfortunatly, the example given by OP is too simple to fully show why this is a bad design, as in this case the "cheese" type only takes a single integer value in all cases.
In order to realize why it is bad, we need to see a more complex example.
This is from something I am working on:
Using the above paradim this is what we end up writing:
class ExperimentRecord():
def __init__(self, experiment_index=None, dictionary=None):
if experiment_index is None and dictionary is None:
raise ExperimentalDatabaseException(f'constructing instance of ExperimentalRecord requires either experiment_index or dictionary to be specified')
elif experiment_index is not None and dictionary is not None:
raise ExperimentalDatabaseException(f'constructing instance of ExperimentalRecoed requires either experiment_index or dictionary to be specified, but not both')
elif experiment_index is None and dictionary is not None:
self.experiment_index = dictionary['index']
self.record_type = dictionary['record_type']
self.data = dictionary['data']
self.measurement_amplitude = dictionary['amplitude']
self.measurement_mean = dictionary['mean']
self.measurement_stddev = dictionary['stddev']
self.measurement_log_likelihood = dictionary['log_likelihood']
elif experiment_index is not None and dictionary is None:
self.experiment_index = experiment_index
self.record_type = None
self.data = None
self.measurement_amplitude = None
self.measurement_mean = None
self.measurement_stddev = None
self.measurement_log_likelihood = None
The resulting code is, to put it bluntly (and I say this as the person who wrote this code), shockingly bad. These are the reasons why:
__init__
has to use complex combinatorial logic to validate the arguments
- if the arguments form a valid combination, then it performs some extensive initialization, in the same function
- this violates the single responsible principle and leads to complex code which is hard to maintain, or even understand
- it can be improved by adding two functions
__init_from_dictionary
and __init_from_experimental_index
but this leads to extra functions being added for really no purpose other than to try and keep the __init__
function managable
- this is totally not how multiple constructors work in languages like Java, C++ or even Rust. Typically we expect function overloading to seperate out the logic for different ways of initializing something into totally independent functions. Here, we mixed everything into a single function, which is the exact opposite of what we want to achieve
Further, in this example, the initialization is dependent only on two variables. But I could have easily added a third:
- For example, we might want to initialize an experimental record from a string or even a filename/path or file handle
- We can imagine that the complexity explodes as more possible methods of initialization are introduced
- In more complex cases, each argument might not be independent. We could imagine possible cases for initialization where valid initializations are formed from a subset of possible arguments, where the subsets overlap somehow in a complex way
For example:
Some object might take arguments A, B, C, D, E
. It might be valid to initialize using the following combinations:
This is an abstract example, because it is hard to think of an simple example to present. However, if you have been around a while in the field of software engineering, you will know that such examples can and do sometimes arrise, regardless of whether their existance points to some shortcominings in the overall design.
With the above said, this is what I am working with, right now. It probably isn't perfect, I have only just started working with Python in a context which required me to write "multiple constructors" as of yesterday.
We fix the problems by doing the following:
- make
__init__
a "null" constructor. It should do the work of a constructor which takes no arguments
- Add constructor functions which modify the object in some way after calling the null constructor (
__init__
)
- Or, if the use case lends itself to inheritance, use an inheritance pattern as others have suggested. (This may or may not be "better" depending on the context)
Something like this, maybe
class ExperimentRecord():
def __init__():
self.experiment_index = None
self.record_type = None
self.data = None
self.measurement_amplitude = None
self.measurement_mean = None
self.measurement_stddev = None
self.measurement_log_likelihood = None
@classmethod
def from_experiment_index(cls, experiment_index):
tmp = cls() # calls `__new__`, `__init__`, unless I misunderstand
tmp.experiment_index = experiment_index
return tmp
@classmethod
def from_dictionary(cls, dictionary):
tmp = cls()
tmp .experiment_index = dictionary['index']
tmp .record_type = dictionary['record_type']
tmp .data = dictionary['data']
tmp .measurement_amplitude = dictionary['amplitude']
tmp .measurement_mean = dictionary['mean']
tmp .measurement_stddev = dictionary['stddev']
tmp .measurement_log_likelihood = dictionary['log_likelihood']
return tmp
With this design, we solve the following problems:
- single responsibility principle: each constructor function is fully independent and does its own thing to initialize the object
- each constructor function takes the arguments it requires for initialization, and nothing more. each possible method of initization requires its own set of arguments, and those sets of arguments are indepenent, and not mashed into one single function call
Note: Since I literally just thought of this, it's possible I have overlooked something. If that is the case please leave a comment explaining the deficiencies and I will try and think of a resolution, and then update the answer. This seems to work for my particular use case but there is always a possibility I have overlooked something, particularly as I didn't know have any need to investigate writing multiple Python constructors until today.