Overview
For the specific cheese example, I agree with many of the other answers about using default values to signal random initialization or to use a static factory method. However, there may also be related scenarios that you had in mind where there is value in having alternative, concise ways of calling the constructor without hurting the quality of parameter names or type information.
Since Python 3.8 and functools.singledispatchmethod
can help accomplish this in many cases (and the more flexible multimethod
can apply in even more scenarios). (This related post describes how one could accomplish the same in Python 3.4 without a library.) I haven't seen examples in the documentation for either of these that specifically shows overloading __init__
as you ask about, but it appears that the same principles for overloading any member method apply (as shown below).
"Single dispatch" (available in the standard library) requires that there be at least one positional parameter and that the type of the first argument be sufficient to distinguish among the possible overloaded options. For the specific Cheese example, this doesn't hold since you wanted random holes when no parameters were given, but multidispatch
does support the very same syntax and can be used as long as each method version can be distinguish based on the number and type of all arguments together.
Example
Here is an example of how to use either method (some of the details are in order to please mypy which was my goal when I first put this together):
from functools import singledispatchmethod as overload
# or the following more flexible method after `pip install multimethod`
# from multimethod import multidispatch as overload
class MyClass:
@overload # type: ignore[misc]
def __init__(self, a: int = 0, b: str = 'default'):
self.a = a
self.b = b
@__init__.register
def _from_str(self, b: str, a: int = 0):
self.__init__(a, b) # type: ignore[misc]
def __repr__(self) -> str:
return f"({self.a}, {self.b})"
print([
MyClass(1, "test"),
MyClass("test", 1),
MyClass("test"),
MyClass(1, b="test"),
MyClass("test", a=1),
MyClass("test"),
MyClass(1),
# MyClass(), # `multidispatch` version handles these 3, too.
# MyClass(a=1, b="test"),
# MyClass(b="test", a=1),
])
Output:
[(1, test), (1, test), (0, test), (1, test), (1, test), (0, test), (1, default)]
Notes:
- I wouldn't usually make the alias called
overload
, but it helped make the diff between using the two methods just a matter of which import you use.
- The
# type: ignore[misc]
comments are not necessary to run, but I put them in there to please mypy
which doesn't like decorating __init__
nor calling __init__
directly.
- If you are new to the decorator syntax, realize that putting
@overload
before the definition of __init__
is just sugar for __init__ = overload(the original definition of __init__)
. In this case, overload
is a class so the resulting __init__
is an object that has a __call__
method so that it looks like a function but that also has a .register
method which is being called later to add another overloaded version of __init__
. This is a bit messy, but it please mypy becuase there are no method names being defined twice. If you don't care about mypy and are planning to use the external library anyway, multimethod
also has simpler alternative ways of specifying overloaded versions.
- Defining
__repr__
is simply there to make the printed output meaningful (you don't need it in general).
- Notice that
multidispatch
is able to handle three additional input combinations that don't have any positional parameters.