After reading this question about returning the "next" string in some alphabet (the digits, followed by upper and lowercase ASCII letters) using Java, I thought, well that's just counting, so why not derive from int
and make a nice representation, which is very easy in Python.
I borrowed the base conversion code from numpy.base_repr
(extending it from max base 36 to 62), but this was not enough. When doing math with these integers, it would be nice if they remained in their base, so I added a class decorator that automatically wraps calls to special methods (in this case the math methods) with the class itself again (taking the base from the first number, in case both have a base). This decorator is adapted from this SO answer.
from functools import wraps
from string import digits, ascii_letters
def wrap_math_methods(cls):
"""Wraps a classes math modules with converter,
so that the result is an instance of the class again."""
methods = {"__abs__", "__add__", "__and__", "__ceil__", "__div__",
"__floor__", "__floordiv__", "__invert__", "__lshift__",
"__mod__", "__mul__", "__neg__", "__or__", "__pos__", "__pow__",
"__radd__", "__rand__", "__rfloordiv__", "__rlshift__",
"__rmod__", "__rmul__", "__ror__", "__rpow__", "__rrshift__",
"__rshift__", "__rsub__", "__rtruediv__", "__rxor__",
"__sub__", "__truediv__", "__xor__"}
def method_wrapper(method):
@wraps(method)
def inner(self, *args, **kwargs):
# Return a new instance of cls, copying the base
return cls(method(self, *args, **kwargs), self.base)
return inner
for attr_name in dir(cls):
if attr_name in methods:
setattr(cls, attr_name, method_wrapper(getattr(cls, attr_name)))
return cls
@wrap_math_methods
class BaseInt(int):
"""An integer class in arbitrary (up to 62) base.
Automatically converts results of math operations."""
digits = digits + ascii_letters
def __new__(cls, value, base=10):
if base > len(cls.digits):
raise ValueError(f"Bases greater than {len(cls.digits)} not handled.")
elif base < 2:
raise ValueError("Bases less than 2 not handled.")
n = super().__new__(cls, value)
n.base = base
return n
def __str__(self):
"""Representation of the number in its base."""
num = abs(self)
res = []
while num:
res.append(self.digits[num % self.base])
num //= self.base
if self < 0:
res.append('-')
return ''.join(reversed(res or '0'))
if __name__ == "__main__":
n = BaseInt(100, 62)
print(n, repr(n))
print(n + 1, repr(n + 1))
print(int(n))
import math
assert isinstance(abs(n), BaseInt), "__abs__"
assert isinstance(n + 1, BaseInt), "__add__"
assert isinstance(BaseInt(0) and 2, BaseInt), "__and__" # short circuit
assert isinstance(math.ceil(n), BaseInt), "__ceil__"
assert isinstance(n / 2, BaseInt), "__div__"
assert isinstance(math.floor(n), BaseInt), "__floor__"
assert isinstance(n // 2, BaseInt), "__floordiv__"
assert isinstance(~n, BaseInt), "__invert__"
assert isinstance(n << 2, BaseInt), "__lshift__"
assert isinstance(n % 2, BaseInt), "__mod__"
assert isinstance(n * 2, BaseInt), "__mul__"
assert isinstance(-n, BaseInt), "__neq__"
assert isinstance(n or 2, BaseInt), "__or__"
assert isinstance(+n, BaseInt), "__pos__"
assert isinstance(n ** 2, BaseInt), "__pow__"
assert isinstance(2 + n, BaseInt), "__radd__"
assert isinstance(2 and n, BaseInt), "__rand__"
assert isinstance(2 // n, BaseInt), "__rfloordiv__"
assert isinstance(2 << n, BaseInt), "__rlshift__"
assert isinstance(2 % n, BaseInt), "__rmod__"
assert isinstance(2 * n, BaseInt), "__rmul__"
assert isinstance(0 or n, BaseInt), "__ror__" # short circuit
assert isinstance(2 ** n, BaseInt), "__rpow__"
assert isinstance(2 >> n, BaseInt), "__rrshift__"
assert isinstance(n >> 2, BaseInt), "__rshift__"
assert isinstance(2 - n, BaseInt), "__rsub__"
# assert isinstance(2 / n, BaseInt), "__rtruediv__"
assert isinstance(2 ^ n, BaseInt), "__rxor__"
assert isinstance(n - 2, BaseInt), "__sub__"
# assert isinstance(n / 2, BaseInt), "__truediv__"
assert isinstance(n ^ 2, BaseInt), "__xor__"
I purposefully wrapped only __str__
and not __repr__
, so that one can still see the underlying integer without calls to int
. Note that int(str(BaseInt(100, 62)), 62)
fails, because int
only supports up to base 36. int(BaseInt(100, 62))
however does not fail, since BaseInt
inherits from int
.
I am interested in general comments as well as comments on how to make defining and testing the math methods easier. Comments on the class decorator are also very welcome (would it have been clearer if I had used a metaclass, for example?).
(This class as is does not quite solve the Java question, mostly because of a different order of the digits, but that is fine with me.)