Sometimes we want some magic in our code and we make classes to work as factories. I’m not fond of this solution but I have seen it often enough in the code. There is one fundamental problem with this solution if not handled correctly. Imagine a Warrior class that can create SmallWarrior or BigWarrior depending on the inputs.
mywarrior = Warrior(big=True)
assert isinstance(mywarrior, Warrior)
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
---> 22 assert isinstance(mywarrior, Warrior)
Most of the simple solutions I have seen fail this assertion and this is very confusing. Clearly we are making an instance of a class but what we have is not it.
class Warrior():
"""factory class that can make specific classes depending on the input"""
def __new__(cls, big):
if big:
return BigWarrior()
else:
return SmallWarrior()
class BigWarrior():
pass
class SmallWarrior():
pass
how can we fix it? the most obvious solution is to inherit small and big warrior from the Warrior class. Problem solved, right? Not really. This is going to create endless loop as our subclasses will also call __new__ from Warrior. The next step to get to the right solution is to detect what we are making in __new__ and act accordingly:
if cls != Warrior:
return super().__new__(cls)
This almost works. if we add some logging we will notice that there still a problem that is hard to notice.
class Warrior():
def __new__(cls, big):
print(f"new - {big}")
if cls != Warrior:
print(f"super - {big}")
return super().__new__(cls)
if big:
print("Big")
return BigWarrior(big)
else:
print("Small")
return SmallWarrior(big)
class BigWarrior(Warrior):
def __init__(self, a):
print(f"init Big - {a}")
class SmallWarrior(Warrior):
def __init__(self, a):
print(f"init Small - {a}")
w = Warrior(True)
this prints:
new - True
Big
new - True
super - True
init Big - True
init Big - True
the init is called twice. In many cases this might not be a problem but this is definitely not desired and a bit unexpected. Why is it happening? When we create instances of our subclasses (small and big warrior) python runs the __init__ on them. This is expected. However python also calls the __init__ after calling __new__ in our factory so we double up. We need to create our instances in a way that doesn’t invoke the init for the second time by calling the __new__ directly on them:
super(Warrior, cls).__new__(BigWarrior)
The final solution:
class Warrior():
def __new__(cls, big):
print(f"new - {big}")
if cls != Warrior:
print(f"super - {big}")
return super().__new__(cls)
if big:
print("Big")
return super(Warrior, cls).__new__(BigWarrior)
else:
print("Small")
return super(Warrior, cls).__new__(SmallWarrior)
class BigWarrior(Warrior):
def __init__(self, a):
print(f"init Big - {a}")
class SmallWarrior(Warrior):
def __init__(self, a):
print(f"init Small - {a}")
w = Warrior(True)
now if we check the type we get expected one:
mywarrior = Warrior(big=True)
assert isinstance(mywarrior, BigWarrior)
assert isinstance(mywarrior, Warrior)