Reputation: 3048
I am trying to force the usage of a more specialized class when a superclass is called with certain parameters. Concretely, I have a Monomial
class (whose __init__
takes the coef
ficient and the power
) and a Constant
class. I wish that whenever Monomial
is called with power=0
, a Constant
instance is returned instead.
The purpose of these classes is to build a framework for generating "random" mathematical functions.
What I had initially:
class Monomial(Function):
def __init__(self, coef: int, power: int, inner=Identity()):
super().__init__(inner)
self.pow = power
self.coef = coef
class Constant(Monomial):
def __init__(self, c: int):
super().__init__(c, 0)
I tried adding the following __new__
method:
class Monomial(Function):
def __new__(cls, coef: int, power: int, inner=Identity()):
if power == 0:
return Constant(coef)
instance = object.__new__(Monomial)
instance.__init__(coef, power, inner)
return instance
The problem is that now whenever a new Constant
is created, the Monomial
's __new__
method is called (with a mismatched signature).
What's the best approach to do this?
Upvotes: 1
Views: 75
Reputation: 110801
The way to return a different class type when calling a class is to override the __new__
method as opposed to __init__
. The value returned by __new__
is the one used as the instance (unlike __init__
which is not even allowed to return a value). You've started correctly, but trying, inside __new__
to instantiate a subclass by calling it, will just re-enter Monomial.__new__
- you are likely getting a recursion error there.
So, even though Python does allow changing the return type of __new__
, maybe you should consider the idea of having a factory function - independent of any class - that will return an instance of the proper class instead. Sometimes "simpler is better".
Anyway, the code for the factory approach is:
class Monomial(Function):
def __init__(self, coef: int, power: int, inner=Identity()):
super().__init__(inner)
self.pow = power
self.coef = coef
class Constant(Monomial):
def __init__(self, c: int):
super().__init__(self, coef=c, power=0)
...
def create_monomial(coef, power):
if power == 0:
return Constant(coef)
return Monomial(coef, power)
(create_monomial can also be a static or class method, as you find better)
And, if you really think it would be better, a way to untangle the __new__
method is:
class Monomial(Function):
def __new__(cls, coef: int, power: int = 0, inner=Identity()):
if power == 0:
cls = Constant
return super().__new__(cls)
class Constant(Monomial):
def __init__(self, c: int, **kw):
super().__init__(c, 0)
Python's instantiating mechanism will call __init__
if the return of __new__
is an instance of self
- so both Monomial and Constant __init__
will properly be called. You just have to fix Constant's __init__
not to break on the ocasional power = 0
parameter it will get.
Fixing the signatures would take considerably more work there, and likely involve the use of a metaclass to actually swallow, in its __call__
method the unused power
to Constant's __init__
;
Also take note that the "real fix" here is that calling super().__new__
requires passing the class explicitly - unlike other uses of super()
where "self" or "cls" are supplied by Python. That is due to __new__
being actually a static method - to which Python adds the cls
when building a class, but through other mechanisms than the used by "classmethods".
Upvotes: 1
Reputation: 8646
How about using factory method
approach? It is a nice alternative, when the exact instance type should be defined dynamically. looks like:
class Monomial(Function):
@staticmethod
def create(coef: int, power: int, inner=Identity()):
if power == 0:
return Constant(coef)
else:
return Monomial(coef, power)
x = Monomial.create(...)
Upvotes: 3