Anakhand
Anakhand

Reputation: 3048

Force usage of specialized subclass

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 coefficient 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

Answers (2)

jsbueno
jsbueno

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

grapes
grapes

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

Related Questions