I can't find a definitive answer for this. As far as I know, you can't have multiple __init__
functions in a Python class. So how do I solve this problem?
Suppose I have a class called Cheese
with the number_of_holes
property. How can I have two ways of creating cheese objects...
parmesan = Cheese(num_holes = 15)
number_of_holes
property: gouda = Cheese()
I can think of only one way to do this, but this seems clunky:
class Cheese():
def __init__(self, num_holes = 0):
if (num_holes == 0):
# Randomize number_of_holes
else:
number_of_holes = num_holes
What do you say? Is there another way?
This question is related to
python
constructor
Use num_holes=None
as a default, instead. Then check for whether num_holes is None
, and if so, randomize. That's what I generally see, anyway.
More radically different construction methods may warrant a classmethod that returns an instance of cls
.
All of these answers are excellent if you want to use optional parameters, but another Pythonic possibility is to use a classmethod to generate a factory-style pseudo-constructor:
def __init__(self, num_holes):
# do stuff with the number
@classmethod
def fromRandom(cls):
return cls( # some-random-number )
The best answer is the one above about default arguments, but I had fun writing this, and it certainly does fit the bill for "multiple constructors". Use at your own risk.
What about the new method.
"Typical implementations create a new instance of the class by invoking the superclass’s new() method using super(currentclass, cls).new(cls[, ...]) with appropriate arguments and then modifying the newly-created instance as necessary before returning it."
So you can have the new method modify your class definition by attaching the appropriate constructor method.
class Cheese(object):
def __new__(cls, *args, **kwargs):
obj = super(Cheese, cls).__new__(cls)
num_holes = kwargs.get('num_holes', random_holes())
if num_holes == 0:
cls.__init__ = cls.foomethod
else:
cls.__init__ = cls.barmethod
return obj
def foomethod(self, *args, **kwargs):
print "foomethod called as __init__ for Cheese"
def barmethod(self, *args, **kwargs):
print "barmethod called as __init__ for Cheese"
if __name__ == "__main__":
parm = Cheese(num_holes=5)
Why do you think your solution is "clunky"? Personally I would prefer one constructor with default values over multiple overloaded constructors in situations like yours (Python does not support method overloading anyway):
def __init__(self, num_holes=None):
if num_holes is None:
# Construct a gouda
else:
# custom cheese
# common initialization
For really complex cases with lots of different constructors, it might be cleaner to use different factory functions instead:
@classmethod
def create_gouda(cls):
c = Cheese()
# ...
return c
@classmethod
def create_cheddar(cls):
# ...
In your cheese example you might want to use a Gouda subclass of Cheese though...
class Cheese:
def __init__(self, *args, **kwargs):
"""A user-friendly initialiser for the general-purpose constructor.
"""
...
def _init_parmesan(self, *args, **kwargs):
"""A special initialiser for Parmesan cheese.
"""
...
def _init_gauda(self, *args, **kwargs):
"""A special initialiser for Gauda cheese.
"""
...
@classmethod
def make_parmesan(cls, *args, **kwargs):
new = cls.__new__(cls)
new._init_parmesan(*args, **kwargs)
return new
@classmethod
def make_gauda(cls, *args, **kwargs):
new = cls.__new__(cls)
new._init_gauda(*args, **kwargs)
return new
Using num_holes=None
as the default is fine if you are going to have just __init__
.
If you want multiple, independent "constructors", you can provide these as class methods. These are usually called factory methods. In this case you could have the default for num_holes
be 0
.
class Cheese(object):
def __init__(self, num_holes=0):
"defaults to a solid cheese"
self.number_of_holes = num_holes
@classmethod
def random(cls):
return cls(randint(0, 100))
@classmethod
def slightly_holey(cls):
return cls(randint(0, 33))
@classmethod
def very_holey(cls):
return cls(randint(66, 100))
Now create object like this:
gouda = Cheese()
emmentaler = Cheese.random()
leerdammer = Cheese.slightly_holey()
Since my initial answer was criticised on the basis that my special-purpose constructors did not call the (unique) default constructor, I post here a modified version that honours the wishes that all constructors shall call the default one:
class Cheese:
def __init__(self, *args, _initialiser="_default_init", **kwargs):
"""A multi-initialiser.
"""
getattr(self, _initialiser)(*args, **kwargs)
def _default_init(self, ...):
"""A user-friendly smart or general-purpose initialiser.
"""
...
def _init_parmesan(self, ...):
"""A special initialiser for Parmesan cheese.
"""
...
def _init_gouda(self, ...):
"""A special initialiser for Gouda cheese.
"""
...
@classmethod
def make_parmesan(cls, *args, **kwargs):
return cls(*args, **kwargs, _initialiser="_init_parmesan")
@classmethod
def make_gouda(cls, *args, **kwargs):
return cls(*args, **kwargs, _initialiser="_init_gouda")
One should definitely prefer the solutions already posted, but since no one mentioned this solution yet, I think it is worth mentioning for completeness.
The @classmethod
approach can be modified to provide an alternative constructor which does not invoke the default constructor (__init__
). Instead, an instance is created using __new__
.
This could be used if the type of initialization cannot be selected based on the type of the constructor argument, and the constructors do not share code.
Example:
class MyClass(set):
def __init__(self, filename):
self._value = load_from_file(filename)
@classmethod
def from_somewhere(cls, somename):
obj = cls.__new__(cls) # Does not call __init__
super(MyClass, obj).__init__() # Don't forget to call any polymorphic base class initializers
obj._value = load_from_somewhere(somename)
return obj
I'd use inheritance. Especially if there are going to be more differences than number of holes. Especially if Gouda will need to have different set of members then Parmesan.
class Gouda(Cheese):
def __init__(self):
super(Gouda).__init__(num_holes=10)
class Parmesan(Cheese):
def __init__(self):
super(Parmesan).__init__(num_holes=15)
This is a pretty clean way I guess and tricky
class A(object):
def __init__(self, e, f, g):
self.__dict__.update({k: v for k,v in locals().items() if k!='self'})
def bc(self):
print(self.f)
k = A(e=5, f=6, g=12)
k.bc() # >>>6
This is how I solved it for a YearQuarter
class I had to create. I created an __init__
which is very tolerant to a wide variety of input.
You use it like this:
>>> from datetime import date
>>> temp1 = YearQuarter(year=2017, month=12)
>>> print temp1
2017-Q4
>>> temp2 = YearQuarter(temp1)
>>> print temp2
2017-Q4
>>> temp3 = YearQuarter((2017, 6))
>>> print temp3
2017-Q2
>>> temp4 = YearQuarter(date(2017, 1, 18))
>>> print temp4
2017-Q1
>>> temp5 = YearQuarter(year=2017, quarter = 3)
>>> print temp5
2017-Q3
And this is how the __init__
and the rest of the class looks like:
import datetime
class YearQuarter:
def __init__(self, *args, **kwargs):
if len(args) == 1:
[x] = args
if isinstance(x, datetime.date):
self._year = int(x.year)
self._quarter = (int(x.month) + 2) / 3
elif isinstance(x, tuple):
year, month = x
self._year = int(year)
month = int(month)
if 1 <= month <= 12:
self._quarter = (month + 2) / 3
else:
raise ValueError
elif isinstance(x, YearQuarter):
self._year = x._year
self._quarter = x._quarter
elif len(args) == 2:
year, month = args
self._year = int(year)
month = int(month)
if 1 <= month <= 12:
self._quarter = (month + 2) / 3
else:
raise ValueError
elif kwargs:
self._year = int(kwargs["year"])
if "quarter" in kwargs:
quarter = int(kwargs["quarter"])
if 1 <= quarter <= 4:
self._quarter = quarter
else:
raise ValueError
elif "month" in kwargs:
month = int(kwargs["month"])
if 1 <= month <= 12:
self._quarter = (month + 2) / 3
else:
raise ValueError
def __str__(self):
return '{0}-Q{1}'.format(self._year, self._quarter)
Those are good ideas for your implementation, but if you are presenting a cheese making interface to a user. They don't care how many holes the cheese has or what internals go into making cheese. The user of your code just wants "gouda" or "parmesean" right?
So why not do this:
# cheese_user.py
from cheeses import make_gouda, make_parmesean
gouda = make_gouda()
paremesean = make_parmesean()
And then you can use any of the methods above to actually implement the functions:
# cheeses.py
class Cheese(object):
def __init__(self, *args, **kwargs):
#args -- tuple of anonymous arguments
#kwargs -- dictionary of named arguments
self.num_holes = kwargs.get('num_holes',random_holes())
def make_gouda():
return Cheese()
def make_paremesean():
return Cheese(num_holes=15)
This is a good encapsulation technique, and I think it is more Pythonic. To me this way of doing things fits more in line more with duck typing. You are simply asking for a gouda object and you don't really care what class it is.
Source: Stackoverflow.com