2

Say I have a Python class as follows:

class TestClass():
    value = 20

    def __str__(self):
        return str(self.value)

The __str__ method will automatically be called any time I try to use an instance of TestClass as a string, like in print. Is there any equivalent for treating it as a number? For example, in

an_object = TestClass()
if an_object > 30:
    ...

where some hypothetical __num__ function would be automatically called to interpret the object as a number. How could this be easily done?

Ideally I'd like to avoid overloading every normal mathematical operator.

Cong Ma
  • 10,692
  • 3
  • 31
  • 47
  • 2
    Provide `__int__()` or `__float__()` according to your need (or `__complex__()`). [Docs](https://docs.python.org/3/reference/datamodel.html#object.__complex__) – kindall Feb 27 '18 at 17:05
  • Not a duplicate, but highly related: https://stackoverflow.com/questions/19750394/dynamically-overriding-functions-in-python – Scott Mermelstein Feb 27 '18 at 17:16
  • I should mention that Python does not convert to numeric type implicitly, while it *does* convert implicitly to strings for some operations. For that, you do need to override all the methods (`__add__()` etc. as well as `__eq__()` etc.). You could write a mixin class for that so as not to have to repeat yourself. – kindall Feb 27 '18 at 17:17
  • @kindall You really should write this up as an answer. I was about to, but you know it better than I. – Scott Mermelstein Feb 27 '18 at 17:19
  • 1
    @ScottMermelstein Done. – kindall Feb 27 '18 at 17:56

3 Answers3

4

You can provide __float__(), __int__(), and/or __complex__() methods to convert objects to numbers. There is also a __round__() method you can provide for custom rounding. Documentation here. The __bool__() method technically fits here too, since Booleans are a subclass of integers in Python.

While Python does implicitly convert objects to strings for e.g. print(), it never converts objects to numbers without you saying to. Thus, Foo() + 42 isn't valid just because Foo has an __int__ method. You have to explicitly use int() or float() or complex() on them. At least that way, you know what you're getting just by reading the code.

To get classes to actually behave like numbers, you have to implement all the special methods for the operations that numbers participate in, including arithmetic and comparisons. As you note, this gets annoying. You can, however, write a mixin class so that at least you only have to write it once. Such as:

class NumberMixin(object):

    def __eq__(self, other):    return self.__num__() == self.__getval__(other)
    # other comparison methods

    def __add__(self, other):   return self.__num__() + self.__getval__(other)
    def __radd__(self, other):  return self.__getval__(other) + self.__num__()
    # etc., I'm not going to write them all out, are you crazy?

This class expects two special methods on the class it's mixed in with.

  • __num__() - converts self to a number. Usually this will be an alias for the conversion method for the most precise type supported by the object. For example, your class might have __int__() and __float__() methods, but __int__() will truncate the number, so you assign __num__ = __float__ in your class definition. On the other hand, if your class has a natural integral value, you might want to provide __float__ so it can also be converted to a float, but you'd use __num__ = __int__ since it should behave like an integer.
  • __getval__() - a static method that obtains the numeric value from another object. This is useful when you want to be able to support operations with objects other than numeric types. For example, when comparing, you might want to be able to compare to objects of your own type, as well as to traditional numeric types. You can write __getval__() to fish out the right attribute or call the right method of those other objects. Of course with your own instances you can just rely on float() to do the right thing, but __getval__() lets you be as flexible as you like in what you accept.

A simple example class using this mixin:

class FauxFloat(NumberMixin):

    def __init__(self, value):     self.value = float(value)
    def __int__(self):             return int(self.value)
    def __float__(self):           return float(self.value)
    def __round__(self, digits=0): return round(self.value, digits)
    def __str__(self):             return str(self.value)

    __repr__ = __str__
    __num__  = __float__

    @staticmethod
    def __getval__(obj):
        if isinstance(obj, FauxFloat):
            return float(obj)
        if hasattr(type(obj), "__num__") and callable(type(obj).__num__):
            return type(obj).__num__(obj)   # don't call dunder method on instance
        try:
            return float(obj)
        except TypeError:
            return int(obj)

ff = FauxFloat(42)
print(ff + 13)    # 55.0

For extra credit, you could register your class so it'll be seen as a subclass of an appropriate abstract base class:

import numbers
numbers.Real.register(FauxFloat)
issubclass(FauxFloat, numbers.Real)   # True

For extra extra credit, you might also create a global num() function that calls __num__() on objects that have it, otherwise falling back to the older methods.

kindall
  • 178,883
  • 35
  • 278
  • 309
0

Looks like you need __gt__ method.

class A:
    val = 0

    def __gt__(self, other):
        return self.val > other


a = A()
a.val = 12
a > 10

If you just wanna cast object to int - you should define __int__ method (or __float__).

bugov
  • 106
  • 5
  • That'll work for one operator, but I'm hoping for a more general solution without having to implement the 15 or so operator override methods for the sake of cleanliness. – Miles Levitsky Feb 27 '18 at 17:11
  • Python has static typing. So you you should override methods or cast one to another. – bugov Feb 27 '18 at 17:15
0

In case of numbers it a bit more complicated. But its possible! You have to override your class operators to fit your needs.

operator.__lt__(a, b)    # lower than

operator.__le__(a, b)    # lower equal

operator.__eq__(a, b)    # equal

operator.__ne__(a, b)    # not equal

operator.__ge__(a, b)    # greater equial

operator.__gt__(a, b)    # greater than

Python Operators

r4r3devAut
  • 90
  • 10