Wrapping integers in Python with Metaclassing



I often require a wrapping integer type in Python, by which I actually mean a subclass of int where all operations are performed modulo some constant number $N$. There are two main use cases for this: 1. Working in a finite field for some cryptographic stuff, or solving problems on [Project Euler](https://projecteuler.net/about). 2. Having Python integers behave like machine registers (8, 16, 32, 64, even 128 bits - you name it.) I decided to solve this once and for all and wrote the integer wrapper class to end all integer wrapper classes. I also managed to keep it rather compact by using *»gasp«* metaclassing. More on that later. Using wrap, you can create new wrapped types like this:
In [2]: class uint32(wrap, bits=32, hexrep=True): pass

In [3]: a = uint32(0xC0C4C01A)

In [4]: a
Out[4]: 0xc0c4c01a

In [5]: a ** 0x00F
Out[5]: 0x2a028000
The hexrep parameter controls whether you want the numbers display in hex rather than the default base 10 that Python uses. I like this for my use case #2, but often not so much for the finite fields. For example, if $F$ is the field where operations happen modulo the prime number $1000000007$, use this:
In [2]: class F(wrap, mod=1_000_000_007): pass

In [3]: a = F(342_211_123)

In [4]: b = a ** -1

In [5]: a * b
Out[5]: 1

In [6]: a / b == a * a
Out[6]: True

In [7]: a // b == a * a
Out[7]: False
The division operator / will use modular inverses while the floor division operator // will work as usual. This is actually a bit of a gotcha, because sometimes you can perform a modular inversion modulo $2^{32}$ and the result is, of course, completely different from floor division:
In [4]: a
Out[4]: 0xc0c4c01a

In [5]: a ** 0x00F
Out[5]: 0x2a028000

In [6]: a / 13
Out[6]: 0x49e7c002

In [7]: a // 13
Out[7]: 0xed40ec6
So, if you hate that, change it. I wanted this to solve two use cases at once, so I am keeping it. Here's the code:
class wrapped(type):
    def __new__(cls, name, bases, nmspc, mod=None, bits=None, hexrep=False, signed=False):
        assert int in bases[0].__mro__
        if None not in (mod, bits) and 1 << bits != mod:
            raise ValueError('incompatible mod and bits argument.')
        mod = mod or bits and 1 << bits
        if mod:
            for op in 'add', 'and', 'floordiv', 'lshift', 'mod', 'mul', 'or', 'rshift', 'sub', 'xor':
                opname = F'__{op}__'
                nmspc[F'__r{op}__'] = nmspc[opname] = lambda self, them, op=getattr(int, opname): (
                    self.__class__(op(self, them)))
            nmspc['__rtruediv__'] = nmspc['__truediv__'] = lambda self, them, p=int.__pow__: (
                self.__class__(self * p(them, -1, mod)))
            nmspc['__pow__'] = lambda self, them, op=int.__pow__: self.__class__(op(self, them, mod))
            nmspc['__inv__'] = lambda self, op=int.__invert__: self.__class__(op(self))
            nmspc['__neg__'] = lambda self, op=int.__neg__: self.__class__(op(self))
            nmspc.update(mod=mod, signed=signed)
            if hexrep is True:
                nib, up = divmod((mod.bit_length() - 1) - 1, 4)
                nib += bool(up)
                nmspc['__repr__'] = lambda self: F'{self:#0{nib}x}'
        return type.__new__(cls, name, bases, nmspc)

    def __call__(cls, value=0, *args, **kwargs):
        if isinstance(value, int):
            value = value % cls.mod
            if cls.signed and (value & (cls.mod >> 1)):
                value -= cls.mod
            return type.__call__(cls, value)
        return cls(int(value, *args, **kwargs))


class wrap(int, metaclass=wrapped):
    pass
I'll explain my choices a little bit: When you create a new integer type, you have to re-implement all of the relevant [standard operators](https://docs.python.org/3/library/operator.html), even though your modification will always be conceptually the same: Perform the operation as the int class would do it, then take the result modulo your given modulus. The metaclass wrapped automates this. When a class of type wrapped is defined, then all of these necessary wrappers are generated by the metaclass and adds them to the class definition. The class wrap is a convenience class to facilitate easier creation of new classes of type wrapped. Fair warning: **Python 3.8** is required for the calculation of multiplicative inverses, because that's when the pow function became able to do this out of the box.

Leave a Reply

Your email address will not be published.