I was writing a class and at some point I decided that it would be nice to have a possibility to create an instance of the class using other instance of this same class. So basically the class would look like this:
class Test:
def __init__(self, a, b):
if type(b) == Test:
self = a / b
else:
self.a = a
self.b = b
def __rtruediv__(self, other):
return Test(other * self.b, self.a)
def __str__(self):
return f"{self.a = }, {self.b = }"
if __name__ == '__main__':
hf = Test(1, 2)
print(hf) # self.a = 1, self.b = 2
print(Test(1, hf)) # AttributeError: 'Test' object has no attribute 'a'
print(1 / Test(1, hf)) # AttributeError: 'Test' object has no attribute 'b'
However, when i tried to do it, I got the AttributeError (AttributeError: 'Test' object has no attribute 'a'). Interestingly enough, the code print(1/Test(1, hf))
gives the same attribute error about attribute b while going into the rtruediv func, so the object Test(1, hf)
has methods that I defined. Why does that happen? Is something wrong with "self = ..."?
CodePudding user response:
The reason this doesn't work is that self
is just an ordinary variable like any other.
if type(b) == Test:
self = a / b
else:
self.a = a
self.b = b
In the if
branch, we reassign a local variable, but it doesn't change the instance being constructed. It just makes a new instance, that will be discarded in a moment. The actual instance being constructed (the original value of self
) is still there and remains in an uninitialized state (i.e. doesn't have a
and b
like it should).
I think your goal here is well-intentioned, though it will likely confuse Python programmers as it's not incredibly idiomatic. My recommendation is to simply set self.a
and self.b
in the constructor in all cases, as it's a more intuitive code design for the average Python coder.
However, what you want can be done. In Python, when you call Test(a, b)
, some complicated internal things happen.
- First, we invoke
__call__
on the metaclass ofTest
. In your example, the metaclass ofTest
isn't specified, so it defaults to [type
], the built-in metaclass. - The default behavior of
type.__call__
is to invoke the class method__new__
on your class (__new__
is implicitly a class method, even if you don't ask it to be, so you don't need the@classmethod
decorator). Your class doesn't currently define__new__
, so we inherit the default one fromobject
. object.__new__
actually creates the new object. This is written in low-level C and can't be replicated in Python. It truly is primitive.- Finally,
object.__new__
callsTest.__init__
on the newly-constructed object and then returns the new object.
Aside from (3), which is truly a C-level primitive, we can easily inject into any part of this. We could write a metaclass and redefine __call__
, but that's overkill for what we're doing. We're going to hook into (2) and define __new__
. So rather than your __init__
, consider
class Test:
def __new__(cls, a, b):
if type(b) == Test:
return a / b
else:
obj = object.__new__(cls)
obj.a = a
obj.b = b
return obj
In __init__
, we're given the object to construct, and our only option is to construct that object (or throw an exception if we can't). But __new__
is lower-level. In __new__
, it's our choice whether and how to create a new object. In the case where we're given two numbers, we delegate to the default __new__
on object
(creating a new object, using the primitive C-level code discussed above), and then we initialize its instance variables. But in the case where b
is a Test
, we do something different and short-circuit the process entirely, calling a different method instead.
Again, I don't necessarily think this is super idiomatic Python code, but it is possible to do what you want.