Python constructor "self = "

Question:

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 = …"?

Asked By: Danny

||

Answers:

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.

  1. First, we invoke __call__ on the metaclass of Test. In your example, the metaclass of Test isn’t specified, so it defaults to [type], the built-in metaclass.
  2. 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 from object.
  3. 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.
  4. Finally, object.__new__ calls Test.__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.

Answered By: Silvio Mayolo
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.