Search code examples
pythoncoercion

how to tell Python that we always want to interpret an object of type Foo as an object of type Bar when there are conflicts?


I am a novice, so please excuse non-standard terminology and let me know if I should add code to make this question more clear.

Let's say that we try to make a class "Rational" in Python. (I know that one is already built in, but ignore that for the purpose of this question.)

We can use __add__ and __mul__, for example, to teach Python how to interpret code of the form a + b or a * b,

where a and b are Rationals.

Now, it may happen that, somewhere else, one wants to compute a + b, where a is a rational but b is an integer. This we can do by modifying our __add__ code within the Rational class to include an if statement, for instance,

def __add__(self, b):
    if isinstance(b, int):
        brat = rational(b, 1)
        return self + brat
    else:
        y = rational(self.num*b.den + b.num*self.den , b.den*self.den)
        y = y.lowest_terms()
        return y

We can similarly modify our __mul__ code, our __div__ code, etc. But there are at least two problems with this kind of solution:

  1. It only works when the second argument is an int. The first argument still has to be a Rational; there's no way to write a method in the Rational class that allows us to add a + b where a is an int and be is a Rational.
  2. It's repetitive. What we really want is some technique be able to say once, globally, in some way, "whenever you are trying to do an operation on multiple objects, some of which are Rationals and some of which are integers, treat the integers as Rationals by mapping n to Rational(n, 1)."

Does such a technique exist? (I have tagged this coercion because I think this is what's called coercion in other contexts, but my understanding is that coercion is deprecated in Python.)


Solution

  • You can avoid the repetition by doing the mapping in the class's initializer. Here's a simple demo that handles integers. Handling floats properly will be left as an exercise for the reader. :) However, I have shown how to easily implement __radd__, and __iadd__, which is the magic method (aka dunder method) that handles +=.

    My code retains rational from your code as the class name, even though class names in Python are conventionally CamelCase.

    def gcd(a, b):
        while b > 0:
            a, b = b, a%b
        return a
    
    class rational(object):
        def __init__(self, num, den=1):
            if isinstance(num, rational):
                self.copy(num)
            else:
                self.num = num
                self.den = den
    
        def copy(self, other):
            self.num = other.num 
            self.den = other.den
    
        def __str__(self):
            return '{0} / {1}'.format(self.num, self.den)
    
        def lowest_terms(self):
            g = gcd(self.num, self.den)
            return rational(self.num // g, self.den // g)
    
        def __add__(self, other):
            other = rational(other)
            y = rational(self.num*other.den + other.num*self.den, other.den*self.den)
            return y.lowest_terms()
    
        def __radd__(self, other):
            return rational(other) + self
    
        def __iadd__(self, other):
            self.copy(self + rational(other))
            return self
    
    
    a = rational(1, 4)
    b = rational(2, 5)
    c = a + b
    print a
    print b
    print c
    print c + 5
    print 10 + c
    c += 10
    print c
    

    output

    1 / 4
    2 / 5
    13 / 20
    113 / 20
    213 / 20
    213 / 20
    

    You may like to reserve that copy method for internal use; the usual convention is to prepend such names with a single underscore.