Home > Back-end >  How to access a parent instance variable using super()
How to access a parent instance variable using super()

Time:09-01

I was playing around a bit with polymorphism and super() in python classes with inheritance and found that I didn't understand the following behavior.

In this dummy example, I found that I can access the instance variables and class variables for the child class, and class variable for the parent class, directly from the object of the child class, BUT I was unsuccessful in accessing the instance variable for the parent class.

I understand that I would have to create an object of the parent class using super() but am unable to access the instance variables from parent class. I don't want to explicitly return anything in the parent.__init__().

The only way I found to do this is to explicitly create an object of the parent class and then fetch the required variable, but that hinders proper abstraction of the code.

Is there a way I can access parent_object._color after its constructor is run, using super(), without explicit instantiation of the parent class object?

#parent class
class hat:
    _color = 'red'                       #Parent class variable
    
    def __init__(self):
        self._color = 'yellow'           #Parent object variable
        

#child class
class cap(hat):
    _color = 'green'                     #Child class variable
    
    def __init__(self, color):           #Child instance variable (passed)
        self._color = color
    
    @property
    def parent_color_super(self):        
        return super()._color            #super() only gives access to class variables
    
    @property
    def parent_color_explicit(self):     
        return hat()._color              #explicit object constructor allows access to instance variable
    
c = cap('blue')

print(c._color)                          #Attribute lookup: Instance variable   
print(c.__class__._color)                #Attribute lookup: class variable 
print(c.__class__.__bases__[0]._color)   #Attribute lookup: parent class variable
print(c.parent_color_super)              #<---
print(c.parent_color_explicit)           #<---

blue
green
red
red        #<--- how to get this as yellow!
yellow     #<---

EDIT:

I have tried super().__init__() but that doesn't let me access the instance variable for the parent class object.

class hat:
    _color = 'red'                       #Parent class variable
    
    def __init__(self):
        self._color = 'yellow'           #Parent object variable
        

class cap(hat):
    _color = 'green'                     #Child class variable
    
    def __init__(self, color):           #Child instance variable (passed)
        self._color = color
    
    @property
    def parent_color_super(self):        #Still doesnt work even after calling __init__!!
        a = super()
        a.__init__()
        return a._color
    
c = cap('blue')

print(c._color)                          #Attribute lookup: Instance variable   
print(c.__class__._color)                #Attribute lookup: class variable 
print(c.__class__.__bases__[0]._color)   #Attribute lookup: parent class variable
print(c.parent_color_super)              #<---
blue
green
red
red #<---

Here as you can see, I am not overwriting any of the variables. The attribute lookup for the object created by super() should access the instance variable for parent class right? This is the behaviour that I dont understand.

CodePudding user response:

There is no separate set of parent class instance variables. The instance variable you are trying to access does not exist. The only instance variable your c has is a single _color instance variable, set to 'blue'.

Your parent_color_explicit does not access what you're trying to access. It creates an entirely separate, completely unrelated instance of hat, and accesses that instance's instance variables.

Heck, you never even called super().__init__ in cap.__init__, so the self._color = 'yellow' assignment never happens at all for c. (It wouldn't solve your problem if you did call super().__init__, because self._color can't be both 'yellow' and 'blue'. Only one of those values would be saved, depending on which assignment happens second.)

CodePudding user response:

Instance variables and methods are fundamentally different beasts. A method, generally speaking, is defined on a class. It happens to be called on an instance, but it doesn't, in any reasonable sense, exist on that instance. More concretely

class Hat:
  _color = 'red'

  def __init__(self, color):
    self._color = color

  def example(self):
    print("I'm a Hat")

class Cap(Hat):
  _color = 'green'

  def __init__(self, color):
    self._color = color

  def example(self):
    print("I'm a Cap")

my_hat = Hat('yellow')
my_cap = Cap('orange')

Let's be perfectly clear about what's happening here. Hat is a class which has three slots defined on it: _color, __init__, and example. Cap is also a class (a subclass of Hat) which has three slots defined on it: _color, __init__, and example. In each case, _color is a string and the other two are functions. Ordinary Python functions at this point, not instance methods. The instance method magic comes later using __get__.

Now, my_hat is a hat. To construct it, we allocated some memory for a new object called my_hat and then called Hat.__init__(my_hat, 'yellow'). This set the _color field on my_hat (which is unrelated to the _color field on Hat at this point). If we were to, at this point, call my_hat.example(), that would end up calling Hat.example(my_hat), through the __get__ magic method on function. Note carefully: At no point is example actually defined on my_hat. It's defined on the class Hat and then the rules of Python allow us to access it as my_hat.example().

Now, with my_cap, we allocate some memory and call Cap.__init__(my_cap, 'orange'). This initializes the instance variable _color to 'orange'. In your current code, we never call the superclass constructor, but even if we did do super().__init__('purple'), the result would get overwritten. The object my_cap can only have _color defined on it once. There's no virtual inheritance for that: it's literally defined on the object, in the exact same way a key is given a value in a dictionary.

The reason super() works for methods is that the parent method still exists on the parent class. Methods aren't called on objects, they're called on classes and happen to take an instance of that class as the first argument. So when we want to access a parent class method, we can simply do Hat.example(my_cap). The super() syntax just automates this process. By contrast, in the case of instance variables that are literally defined on the object, not on a class, the old value is gone as soon as you overwrite it. It never existed at that point. The fact that there are class-level variables with the same name is irrelevant, those serve no purpose to an instance which has already defined a field with the same name.

If you want virtual inheritance, you need names to be defined on the class. So you need functions, or if you want the property syntax, you need @property. At some point, you're going to have to access a (real, concrete, non-virtual) backing field, but you can hide that behind @property if you like.

class Hat:

  def __init__(self):
    self._Hat_color = 'yellow'

class Cap(Hat):

  def __init__(self, color):
    super().__init__()
    self._Cap_color = color

  @property
  def hat_color(self):
    return self._Hat_color

my_hat = Hat('yellow')
my_cap = Cap('orange')

If you want to automate the process of mangling instance variable names from child classes even further, you can use dunder variables. self.__color will get renamed to self._Hat__color internally if used inside of Hat. It's just a trick of naming, not any deeper magic, but it can help with avoiding conflicts.

CodePudding user response:

You are missing the point of how method inheritance works. Since your child class defines an __init__ method that does not call super().__init__, the line self._color = 'yellow' will never be called.

There are a couple of things you can do. One option is to call super().__init__() somehow:

def __init__(self, color):
    self._color = color
    super().__init__()

Now of course you lose the input variable color since it gets overwritten by 'yellow' no matter what.

Another alternative is to call super().__init__() on the instance. __init__ is just a regular method as far as you are concerned, so you don't need to call an allocator to call it:

@property
def parent_color_explicit(self):
    color = self._color
    super().__init__()
    color, self._color = self._color, color
    return color

The point is that an instance contains one __dict__. That dictionary can contain one single instance of the _color key. You can access the __dict__ of parent classes via super(), but for the most part that will give you access to a bunch of alternative method implementations. Those implementations still operate on the one and only instance dictionary.

If you want to have multiple definitions of a thing, name it something else in instances of the child class.

  • Related