Search code examples
pythonformatmagic-methods

Python class % operator overloading


I have a set of functions that calculate different numeric characteristics (in example named calculate) but some of them might not be calculate properly. After calculation result will be print in graphic user interface with string formatting. So I would like to return an object of special class that can be formatted as float but return constant value e.g. N/A (_WrongCalculationError). Is there any magic method in Python to do so using oldstyle formatting?

class _WrongCalculationError(object):
    def __??????__(self):
        return "N/A"

WrongCalculationError = _WrongCalculationError()

def calculate(x, y):
    if x == y:
        return WrongCalculationError
    else:
        return x/y

 def main(*args):
     print("Calculation result is: %.3f" % calculate(args[0], args[1])

I read about __format__ method but I would not like to use new-style formatting because in my point of view it's too complicated and hard to read.


Solution

  • Short answer: You should raise (not return) errors, and they should inherit from Exception:

    class WrongCalculationError(Exception):
        pass
    
    def calculate(x, y):
        if x == y:
            raise WrongCalculationError
        return x / y
    

    Then you can handle them like:

    try:
        print("Calculation result is: %.3f" % calculate(args[0], args[1]))
    except WrongCalculationError:
        print("Oops!")
    

    Long answer: The "magic methods" for modulo % are __mod__, __rmod__ and __imod__:

    >>> class Test(object):
        def __mod__(self, other):
            print "foo:", other
    
    
    >>> t = Test()
    >>> t % "bar"
    foo: bar
    

    However, this is really just a syntax hack; it only looks a bit like C-style string formatting. If you want to pass your custom object as an value to % (i.e. on the right-hand side) this will not work; __rmod__ is not the same:

    >>> class Test(object):
        def __rmod__(self, other):
            raise Exception
    
    
    >>> t = Test()
    >>> "bar %s" % t
    'bar <__main__.Test object at 0x030A0950>'
    >>> "bar %f" % t
    
    Traceback (most recent call last):
      File "<pyshell#11>", line 1, in <module>
        "bar %f" % t
    TypeError: float argument required, not Test
    

    Note that:

    1. __rmod__ is never called; and
    2. TypeError occurs on a '%f' conversion specification.

    You can't customise C-style string formatting; the only exception is that '%s' will call __str__, which you can implement, but you certainly can't give a non-float as a value for '%f'. By comparison, you can totally mess around with str.format:

    >>> class Test(object):
        def __format__(self, spec):
            return "hello!"
    
    
    >>> "{0:03f}".format(Test())
    'hello!'
    

    Edit: As Martijn points out in the comments you could implement __float__ to provide a value to '%f', but this has to return a float.