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. <span id="more-5322"></span> 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: ```python 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. Required fields are marked *