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
CodePudding user response:
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)
CodePudding user response:
You could create an iterator object and utilize next()
:
class AutomaticCounter:
def __iter__(self):
self.a = 1
return self
def __next__(self):
out = self.a
self.a =1
return out
n = iter(AutomaticCounter())
print(next(n)) # 1
print(next(n)) # 2
print(next(n)) # 3
CodePudding user response:
There are two issues here. First, self
isn't actually the object but rather a variable reference to the object. When you reassign self
, you're not changing the object but merely causing the self
variable, which only has local scope, to now reference some other object. The original object remains unchanged.
Second, unless you really know what you're doing (and I don't), it is, in my opinion, unadvisable to subclass immutable built-ins. What you can do is have the object have an integer attribute and then define the __getattr__
method to pass any attribute calls on to the integer.
class AutomaticCounter:
def __init__(self, start=0):
self.item = start
def __str__(self):
self.item = 1
return str(self.item)
def __getattr__(self, attr):
return getattr(self.item, attr)