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
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)