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.