Adding a numeric type between Real and Rational, and supporting the type's functionality for Rational numbers

StackOverflow https://stackoverflow.com/questions/22737760

Question

Python offers a set of abstract base classes for types of numbers. These start with Number, of which Complex is a subclass, and so on through Real, Rational and Integral. Since each is a subclass of the last, each supports the special functionality of the classes that came before it in the sequence. For example, you can write (1).numerator to get the numerator of the Python integer 1, created using the integer literal 1, considered as a rational number.

The linked page notes: There are, of course, more possible ABCs for numbers, and this would be a poor hierarchy if it precluded the possibility of adding those. You can add MyFoo between Complex and Real with:

class MyFoo(Complex): ...
MyFoo.register(Real)

This has the effect of adding a new subclass of complex numbers such that objects of type Real will test as being instances of the new class - thus adding the new class "in between" Complex and Real in some sense. This doesn't address, however, the possibility that the new class might introduce functionality (such as that exemplified by the numerator property) not offered by its subclass.

For example, suppose that you want to add a class whose instances represent numbers of the form a + b√2 where a and b are rational numbers. You would probably represent these numbers internally as a pair of Fractions (instances of fraction.Fraction from the Python standard library). Evidently, this class of numbers is properly a subclass of Real, and we would want to treat Rational as being its subclass (because every rational number is a number of our new type in which b == 0). So we would do this:

class FractionWithRoot2Part (Real): ...
FractionWithRoot2Part.register(Rational)

We might want to add properties to the new class that (say) return the numbers a and b. These properties might be called something like RationalPart and CoefficientOfRoot2. This is awkward, however, because existing numbers of type Rational will not have these properties. If we write (1).RationalPart then we will get an AttributeError. Demonstration:

Python 3.3.1 (v3.3.1:d9893d13c628, Apr  6 2013, 20:25:12) [MSC v.1600 32 bit (In
tel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from abc import *
>>> class c1 (metaclass = ABCMeta):
...     def x (self): return 5
...
>>> class c2: pass
...
>>> c1.register(c2)
<class '__main__.c2'>
>>> a1 = c1()
>>> a2 = c2()
>>> a1.x()
5
>>> a2.x()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'c2' object has no attribute 'x'

Thus we have not truly introduced a new type that is "in between" the existing two types, because the type "at the bottom" of the subclass relation does not support the behaviours of the class "in the middle".

What is the generally accepted way to get around this? One possibility is to provide a function (not a method of any class) which can handle any kind of input and act intelligently; something like this:

def RationalPart (number):
    if isinstance(number, FractionWithRoot2Part):
        try:
            return number.RationalPart
        except AttributeError:
            # number is presumably of type Rational
            return number
    else:
        raise TypeError('This is not supported you dummy!')

Is there a better way than this?

Was it helpful?

Solution

You can't use ABCs to modify existing classes in this way (and in fact, you can't safely modify existing classes in this way at all). ABCs are only a mechanism for customizing whether a class tests as a subclass of another (and instances of it test as instances of the other), not for actually changing subclass implementations. When the documentation talks about defining a new class "in between", this is the sense it means; it just means in between in terms of subclass/instance checks, not actual inheritance. This is described here:

ABCs introduce virtual subclasses, which are classes that don’t inherit from a class but are still recognized by isinstance() and issubclass()

Note what it says: the virtual subclasses don't actually inherit from your ABC, they just test as if they do. That's how ABCs are designed to work. The way to use them is suggested here:

An ABC can be subclassed directly, and then acts as a mix-in class.

So you can't modify the existing Rational class using ABC. The way to do what you want is to make a new class that inherits from Rational and uses your ABC as a mixin. Then use that class instead of the regular Rational.

In fact, you might not even really need to use ABCs here. The only advantage of using ABCs is that it makes your new rational-like numbers look like Rationals if anyone explicitly tests; but as long as you inherit from Rational and from your new class that adds the behavior you want, the new classes will act like Rationals anyway.

When you say

This is awkward, however, because existing numbers of type Rational will not have these properties.

you have targeted the essence of the situation. It might seem awkward, but it would also be mighty awkward if someone could come in and, with an ABC end-run, start modifying the behavior of your existing classes by sticking a new superclass above them in the inheritance hierarchy. That's not how it works. There is no safe way to add new behavior to existing instances of any class; the only safe thing is to add new behavior to your new class, and tell people to use that new class instead of the old class.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top