6

I'm trying to create a custom class that behaves like an integral numeric type. The straightforward way to do that is to consult Python3 documentation on the matter, and then implement all the magic functions: both arithmetic __add__, __radd__, __sub__, ... and comparison __le__, __lt__, ... operators.

However, implementing them all by hand is tedious. I believe that in many cases, a good enough solution would be to implement them automatically based on the __int__ magic function: we simply do the arithmetic with the object converted to int. Is this part of any commonly used library? What I'm looking for is something similar to @functools.total_ordering, which automatically derives all comparison operators from only one of the operators.

If there is no such thing, then why? Would it be a bad idea to have such automatic conversion, or is it simply an issue one does not encounter too often?

Edit: In case that the details of the custom class are relevant, I provide some of them here.

What I'm constructing is a counter whose value can change not only through arithmetic operations, but also other means. These can be very general: for example, we can tell the counter the following: "If anybody asks you what value you represent, return twice the normal value."

To illustrate, suppose you're playing a board game, for example, similar to Civilization. The game makes use of many parts, one of them is the counter that counts the military strength your civilization has. However, there may be an in-game effect which causes each point of strength to count twice.

Since the class is required to be mutable, I believe subclassing int is not an option (as int is immutable).

4
  • 2
    We need more details. What other functionality will your custom class have? Depending on that, we can offer suggestions. Commented Aug 18, 2018 at 19:55
  • Maybe you could just subclass int? Commented Aug 18, 2018 at 20:05
  • 1
    How about a mixin class that implements the operators once? Commented Aug 18, 2018 at 20:05
  • Looked up mixin, looks like a good enough solution. @Aran-Fey: The number that the class represents is not always the same, it changes based on the state (it is mutable). Thus, subclassing int is not an option. Commented Aug 18, 2018 at 20:14

1 Answer 1

2

You can write your own mixin class or decorator that does this, similarly to the way collections.abc.Sequence implements a bunch of Sequence methods on top of a handful that you write manually, or functools.total_ordering implements a bunch of comparison methods for you. (If you're wondering how to decide which one to use: if the code has to examine your class before modifying it, as with total_ordering, your mixin would need a custom metaclass, so a decorator is easier. Otherwise, a mixin is usually simpler.)

There are dozens of implementations of this idea floating around PyPI and the ActiveState recipes and random blog posts and GitHub repos. But the problem isn't quite generalizable to turn a solution into a widely-used library.

As for the specific case you want—an object that acts like a mutable int, and implements it on the basis of its __int__ method—I think that specific case just doesn't come up often enough that anyone has sat down and written it.


Anyway, getting all of this right—including things like 3.0 + x and x + 3.0—is a bit of a pain.

Implementing the arithmetic operations in the numbers documentation explains exactly what you want to do to get it right.

It also contains an explanation of the source code used by the fractions library to reduce the boilerplate and repetition of defining all of these methods on fraction.Fraction, which is very handy.

The only problem with all of that is that it's written for immutable numeric types, not mutable ones. So, if you want to use it, you'll need to modify it, with an operator_fallback function that builds and returns forward, reverse, inplace instead of just forward, reverse.


However, your case is simpler than Fraction, so you can simplify the operator-function-creating methods, like this:

def _operator_fallbacks(op, sym):
    def forward(a, b):
        result = op(int(a), b)
        if isinstance(result, int): result = type(a)(result)
        return result
    forward.__name__ = '__' + op.__name__ + '__'
    forward.__doc__ = f"a {sym} b"

    def reverse(b, a):
        result = op(a, int(b))
        if isinstance(result, int): result = type(b)(result)
        return result
    reverse.__name__ = '__r' + op.__name__ + '__'
    reverse.__doc__ = f"a {sym} b"

    def inplace(a, b):
        # If you want to work via __int__ rather than by digging
        # into the internals, you can't delegate += to +=, you
        # have to implement it via some kind of set method.
        a.set(op(int(a), b))
        return a
    inplace.__name__ = '__i' + op.__name__ + '__'
    inplace.__doc__ = f'a {sym}= b'

    return forward, reverse, inplace

__add__, __radd__, __iadd__ = _operator_fallbacks(operator.add, '+')
__sub__, __rsub__, __isub__ = _operator_fallbacks(operator.sub, '-')

You might want to consider isinstance(result, numbers.Integral). You might also want to make your class a subtype of numbers.Integral. If you do both, be careful about how you construct them, so you don't end up with an Int(Int(Int(5))) instead of an Int(5).

If you wanted to generalize this into a mixin that can be used for mutable int, float, complex, Decimal, Fraction, etc. types, you could just add a get method that all of your types have to implement, instead of relying on __int__. Although you'd have to think through the conversion issues (int is easy, being on the base of the numeric tower).

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.