2

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 2:

Based on the valuable answers from @Silvio Mayolo, @user2357112, and @Mad Physicist, I think I understand where the issue was.

As I understand, the child class's instance c will hold the instance variable _color which will get overwritten by the super().__init__() as the job of that function is to update the self._color of whichever object its called from.

This means, as clarified by the answers, there is no separate "parent instance variables" that can be accessed by super(); only class attributes such as the class variable _color (red) and __init__() (which sets the instance variable _color to (yellow).

A modified example to show this behavior is as follows -

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

class cap(hat):
    _color = 'green'                     #Child class variable
    
    def __init__(self, color):           #Child instance variable (passed)
        self._color = color
    
    def parent_color_super(self):        #Call __init__ from parent, overwrites _color
        super().__init__()
    
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
c.parent_color_super()                   #Call init from super class
print(c._color)                          #Attribute lookup: Updated Instance variable
blue
green
red
yellow
Akshay Sehgal
  • 18,741
  • 3
  • 21
  • 51

3 Answers3

1

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

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Right, that makes sense. But if I want to still access the parent class from inside a child method and return the instance variable from it, is there a way? (check `c.parent_color_super`) ... EDIT: Exactly (to your second point) I understand that. That is why I wanted to replicate this behaviour using `super()` in previous function. – Akshay Sehgal Sep 01 '22 at 02:47
  • @AkshaySehgal: No, because instance variables don't work like that in Python. There's no separate "parent instance `_color`" and "child instance `_color`"; an object only has a single instance variable namespace. – user2357112 Sep 01 '22 at 02:52
  • If you want the parent class and the child class to use distinct instance variables, give them different names. Don't use the same name for everything. – user2357112 Sep 01 '22 at 02:54
  • Thanks for the amazing comments! I wanted to understand this exactly. I am calling them the same name specifically to test this behavior on purpose. I understand that that `__init__` is required after using `super()` but I have tried that but still doesn't behave as expected. Could you help me understand a bit further? Please do check my edit. – Akshay Sehgal Sep 01 '22 at 02:57
1

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.

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • `The reason super() works for methods is that the parent method still exists on the parent class.` .. This makes a lot of sense. So then as I understand it, `super()` allows access only to class attributes (which can be class variables or methods). Is this correct? – Akshay Sehgal Sep 01 '22 at 03:06
  • @AkshaySehgal. Don't confuse class attributes for instance attributes. Each class object in the MRO has its own dict, which is why `super()` works, but the instance has only one dict, no matter how many layers of inheritance its class has. – Mad Physicist Sep 01 '22 at 03:09
1

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.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • Thanks a ton! This makes perfect sense. Especially the last example. As @user2357112 also pointed out, the `self._color` cant be both blue and yellow, and that there is nothing as `parent class instance variable` that can be separately accessed as a `dict` from inside the child class, atleast using `super()`. When I am calling `super().__init__()` I am simply overwriting `self._color` (which belongs to child) as defined by `parent.__init__()`. Am I right in this understanding? – Akshay Sehgal Sep 01 '22 at 03:21
  • @AkshaySehgal. Yes. Think of `self` as a bag of data, which is stored in `__dict__`. The instance is just data. The class is a set of methods and other attributes that interprets and operates on that data. `super()` allows you to select a different interpretation for the data, but besides that, the instance does not care about the class. `self._color` does not belong to the child, it belongs to the instance. Hopefully that makes it easier to understand how things are manipulated. – Mad Physicist Sep 01 '22 at 06:48
  • Makes sense, I think my basics on super() were shaky. While I understand how self works, this clarifies a lot about how super() behaves. Thanks a ton! – Akshay Sehgal Sep 01 '22 at 16:32
  • 1
    `super` just says "grab the implementation from the dict of the class one step up the MRO". It does nothing to the instance data. – Mad Physicist Sep 01 '22 at 16:41
  • Makes sense. I think my biggest confusion was due to some of the online guides referring to `super()` as **a quick way to create an object of parent class inside child class**, which is where I was trying to see if I can access a parent instance variable. Seems this is not the case! – Akshay Sehgal Sep 01 '22 at 16:45
  • @AkshaySehgal. It's all consistent when you realize that instance data can't and doesn't have a parent. – Mad Physicist Sep 01 '22 at 16:52