1

A similar question was asked in Initialize subclass within class in python. The answers there concluded that this kind of approach should be avoided, I am not sure if this is true for the following case, and I would like to know either how it can be achieved, or what I should do instead.

class Rational():
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator
        if self.denominator == 1:
            pass
            # >>> How to initialize as Integer in this case? <<<

    # ... More methods for rationals


class Integer(Rational):
    def __init__(self, value):
        super().__init__(value, 1)
        self.value = value

    # ... More methods for integers

In this file, I have simple classes for Integer and Rational numbers. Since all Integers are rational, Integer is a subclass of rational, and we call super() to allow methods for rational numbers to be called by the integer.

It would be nice though, if whenever a Rational was initialized with denominator 1, it could be automatically recognized as an integer. For example, I would want x = Rational(4, 1) to then allow me to call x.integer_method() or even have Integer(4) == x return True. The trouble is, I don't know if I can call the Integer initializer from the Rational initializer because I may be caught in an infinite loop.

What is the best way to resolve this (in a general way that works not just for integers and rationals, but for any Parent-Child type where an instance of Parent might not be recognized as semantically equivalent to a member of Child type until initialization time?

MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
  • 2
    Sounds like a textbook usecase for a factory method for replacing the constructor. – rdas Jun 17 '20 at 19:40
  • 1
    If you define a `__new__()` method for the `Rational` class, it can return an instance of `Integer` when the conditions are met. See my answer to the question [Improper use of \__new__ to generate classes?](https://stackoverflow.com/questions/28035685/improper-use-of-new-to-generate-classes) for an example. – martineau Jun 17 '20 at 19:56
  • Poor design pattern, not very SoLID – Pynchia Jun 17 '20 at 19:58
  • @Pynchia: Not the greatest comment because it assumes everyone knows what [SOLID](https://en.wikipedia.org/wiki/SOLID) means. – martineau Jun 17 '20 at 20:02

1 Answers1

2

Use __new__ to define how the class is constructed, including constructing other classes. Avoid defining __init__, since it is not automatically called when __new__ returns an object of another type. As this scheme strongly couples classes together, Integer can avoid calling super().__new__ for simplicity.

class Rational():
    def __new__(cls, numerator, denominator=1):
        if denominator == 1:
            return Integer(numerator)
        self = object.__new__(cls)
        self.numerator = numerator
        self.denominator = denominator
        return self   # new always returns an instance

    def __repr__(self):
        return f'Rational({self.numerator}/{self.denominator})'

class Integer(Rational):
    denominator = 1  # as a Rational, Integer must expose numerator and denominator
    def __new__(cls, value):
        self = object.__new__(cls)
        self.numerator = value
        return self

    def __repr__(self):
        return f'Integer({self.numerator})'

This is enough to dynamically construct the appropriate subclass:

>>> Rational(12, 3)
Rational(12/3)
>>> Rational(15, 1)
Integer(15)

Ideally, such classes should be immutable; otherwise, Integer being a Rational implies that some_integer.denominator = 3 is valid and produces a Rational with 1/3 the initial value.

MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119