Search code examples
pythonsubclass

Automatic counter as a subclass of integer?


Is it possible to create a class of integer where an instance of a certain class (say AutomaticCounter) will increase itself by some number (say 1) each time it is called?

>>> n = AutomaticCounter(start=0)
>>> print(n)
1
>>> print(n)
2

This is what I have tried so far:

class AutomaticCounter(int):
    def __init__(self):
        self = 0
    def __str__(self):
        self = self + 1
        return self

Solution

  • If you really, really, really need to mangle an immutable and built-in type, then you can create a kind-of "pointer" to it:

    class AutomaticCounter(int):
        def __new__(cls, *args, **kwargs):
            # create a new instance of int()
            self = super().__new__(cls, *args, **kwargs)
            # create a member "ptr" and storing a ref to the instance
            self.ptr = self
            # return the normal instance
            return self
    
        def __str__(self):
            # first, create a copy via int()
            # which "decays" from your subclass to an ordinary int()
            # then stringify it to obtain the normal __str__() value
            value = str(int(self.ptr))
    
            # afterwards, store a new instance of your subclass
            # that is incremented by 1
            self.ptr = AutomaticCounter(self.ptr + 1)
            return value
    
    n = AutomaticCounter(0)
    print(n)  # 0
    print(n)  # 1
    print(n)  # 2
    
    # to increment first and print later, use this __str__() instead:
        def __str__(self):
            self.ptr = AutomaticCounter(self.ptr + 1)
            return str(int(self.ptr))
    

    This, however, doesn't make the type immutable per se. If you do print(f"{self=}") at the beginning of __str__() you'll see the instance is unchanged, so you effectively have a size of 2x int() (+ some trash) for your object and you access the real instance via self.ptr.

    It wouldn't work with self alone as self is merely a read-only reference (created via __new__()) passed to instance's methods as the first argument, so something like this:

    def func(instance, ...):
        instance = <something else>
    

    and you doing the assignment would, as mentioned by Daniel, simply assign a new value to the local variable named instance (self is just a quasi-standard name for the reference) which doesn't really change the instance. Therefore the next solution which looks similar would be a pointer and as you'd like to manipulate it the way you described, I "hid" it to a custom member called ptr.


    As pointed out by user2357112, there is a desynchronization caused by the instance being immutable, therefore if you choose the self.ptr hack, you'll need to update the magic methods (__*__()), for example this is updating the __add__(). Notice the int() calls, it converts it to int() to prevent recursions.

    class AutomaticCounter(int):
        def __new__(cls, *args, **kwargs):
            self = super().__new__(cls, *args, **kwargs)
            self.ptr = self
            return self
    
        def __str__(self):
            value = int(self.ptr)
            self.ptr = AutomaticCounter(int(self.ptr) + 1)
            return str(value)
    
        def __add__(self, other):
            value = other
            if hasattr(other, "ptr"):
                value = int(other.ptr)
            self.ptr = AutomaticCounter(int(self.ptr) + value)
            return int(self.ptr)
    
        def __rmul__(self, other):
            # [1, 2, 3] * your_object
            return other * int(self.ptr)
    
    n = AutomaticCounter(0)
    print(n)    # 0
    print(n)    # 1
    print(n)    # 2
    print(n+n)  # 6
    

    However, anything that attempts to pull the raw value or tries to access it with C API will most likely fail, namely reverse operations e.g. with immutable built-ins should be the case as for those you can't edit the magic methods reliably so it's corrected in all modules and all scopes.

    Example:

    # will work fine because it's your class
    a <operator> b -> a.__operator__(b)
    vs
    # will break everything because it's using the raw value, not self.ptr hack
    b <operator> a -> b.__operator__(a)
    

    with exception of list.__mul__() for some reason. When I find the code line in CPython, I'll add it here.


    Or, a more sane solution would be to create a custom and mutable object, create a member in it and manipulate that. Then return it, stringified, in __str__:

    class AutomaticCounter(int):
        def __init__(self, start=0):
            self.item = start
        def __str__(self):
            self.item += 1
            return str(self.item)