2

I'm trying to create a class MyFloat that is very similar to float but has a value method and a few other methods. I also need instances of this class to interact with real floats and be "sticky" in the sense that type(MyFloat(5) + 4) is MyFloat and not float.

So this is what I have so far:

class MyFloat(numbers.Real):
    def __init__(self, value):
        self._value = float(value)

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, value):
        self._value = float(value)

    def __float__(self):
        return self.value

but now I have to implement a ton of other methods in order to satisfy the abstract base class numbers.Real and also my requirement that this "acts" like a float. The issue is that a) there's a ton of these methods (__add__, __trunc__, __floor__, __round__, __floordiv__... I think 24 in total) and b) they all kinda follow the same pattern. That is, for any method, I probably just want:

def __method__(self):
    return MyFloat(float.__method__(float(self))

or

def __method__(self, other):
    return MyFloat(float.__method__(float(self), float(other)))

Is there a way to do what I want without manually implementing every math method (and repeating a ton of code)?

Keep in mind that float.__method__ doesn't actually exist for every method. For example, float.__floor__ doesn't exist, but I still need to implement MyFloat.__floor__ to satisfy numbers.Real abstract base class.

Edit: Suggestions have been to extend float and that was also my first idea but I lose my "stickiness" requirement. For example:

>>> class MyFloat(float):
    def other_method(self):
        print("other method output")

>>> mf = MyFloat(8)
>>> mf.other_method()
other method output
>>> o = mf + 9
>>> type(o)
<class 'float'>
>>> o.other_method()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'float' object has no attribute 'other_method'
bkanuka
  • 907
  • 7
  • 18
  • Are you asking if there is a way to create a duplicate of an existing object type without manually implementing all of the methods? – user1558604 Dec 07 '19 at 15:21
  • You can do some trickery with either `__getattr__` or `exec` and string formatting in a loop. – Michael Butscher Dec 07 '19 at 15:26
  • @user1558604 no, I'm aware I could inherit from float, but I lose my "value" method and my "sticky" requirement. (afaik) @MichaelButscher I ain't touching `exec` ;-) – bkanuka Dec 07 '19 at 15:29
  • If you want to subclass float, why not actually do that: `class MyFloat(float):` and then in the init function `super().__init__(value)`. That way all of the float class’s properties and methods are also in your class – Jim Danner Dec 07 '19 at 15:45
  • If you extend float, you should be able to use all of its functions and add your own on top. – user1558604 Dec 07 '19 at 15:49
  • Regarding extending float, I'd like to, but I think I lose the stickiness I need. I'm going to edit my question with an example. – bkanuka Dec 07 '19 at 15:52
  • An idea would be to write a method `__getattribute__` that would catch all method calls and analyze them to pass them on to the superclass in the way you suggested. But apparently that doesn’t work on methods that implement operators, like `__add__` which implements +. Decorators can be used instead, says https://stackoverflow.com/a/16372436/7840347 – Jim Danner Dec 07 '19 at 16:18
  • @JimDanner thanks for finding that. It has an accepted answer, but doing this still requires a bunch of manual work. Some methods are under `operator`, some under `math` and some on `float`. Somehow this doesn't have the clear elegance I was looking for, ya know? :-/ – bkanuka Dec 07 '19 at 16:32
  • Classes are where Python loses its elegant simplicity :-( – Jim Danner Dec 07 '19 at 16:37

1 Answers1

0

Okay so this is the most "elegant" solution I've come up with (even though it's hacks on hacks). If anyone has a suggestion for improvement I'm happy to select yours as the correct answer.

import numbers
import math
import operator


def _operator_builder(numbers_base_cls, builtin_type):
    def _binary_method_builder(builtin_operator):
        def binary_method(a, b):
            if isinstance(b, numbers_base_cls):
                return type(a)(builtin_operator(builtin_type(a), builtin_type(b)))
            else:
                return NotImplemented

        binary_method.__name__ = '__' + builtin_operator.__name__ + '__'
        binary_method.__doc__ = operator.__doc__
        return binary_method

    def _monary_method_builder(builtin_operator):
        def monary_method(a, *args):
            return type(a)(builtin_operator(builtin_type(a), *args))

        monary_method.__name__ = '__' + builtin_operator.__name__ + '__'
        monary_method.__doc__ = operator.__doc__

        return monary_method
    return _binary_method_builder, _monary_method_builder


_float_binary_operator_helper, _float_monary_operator_helper = _operator_builder(numbers.Real, float)


class MyFloat(numbers.Real):
    __add__ = _float_binary_operator_helper(operator.add)
    __radd__ = __add__

    __trunc__ = _float_monary_operator_helper(math.trunc)
    __floor__ = _float_monary_operator_helper(math.floor)
    __ceil__ = _float_monary_operator_helper(math.ceil)
    __round__ = _float_monary_operator_helper(round)
    # etc... for all dunder methods

    def __init__(self, value):
        self._value = float(value)

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, value):
        self._value = float(value)

    def __float__(self):
        return self.value

One improvement would be to define a list of (name, func) pairs and call setattr(name, _float_monary_operator_helper(func)) on each of them during __new__. However, I couldn't figure out how to do this and make ABC happy - it always complained that the methods weren't implemented.

bkanuka
  • 907
  • 7
  • 18