5

I know that by inheriting the base class. All the functions in base class would be accessible in the derived class as well. But how does it work the other way, meaning can a function that is defined in the child class be accessible in the base class.

I tried the above out with an example. And it works just fine. But how can that be. I am not able to get the logic behind the working.

class fish:

    def color(self):
        # _colour is a property of the child class. How can base class access this?
        return self._colour  


class catfish(fish):

    _colour = "Blue Cat fish"

    def speed(self):
        return "Around 100mph"

    def agility(self):
        return "Low on the agility"

class tunafish(fish):

    _colour = "Yellow Tuna fish"

    def speed(self):
        return "Around 290mph"

    def agility(self):
        return "High on the agility"

catfish_obj = catfish()
tunafish_obj = tunafish()

print(catfish_obj.color())
print(tunafish_obj.color())

I understand that the instance is being passed and all through self, but the details of the child class should logically not be accessible in the base class, correct?!

Abhishek
  • 155
  • 3
  • 14
  • No, the methods of a derived class are not available in the base. – Martijn Pieters Mar 24 '18 at 15:18
  • The output of the above program is : Blue Cat fish Yellow Tuna fish. (So this would prove otherwise) – Abhishek Mar 24 '18 at 15:19
  • To clarify Martijn's comment: `_colour` _is_ an attribute of the class, but it's _accessed_ through an instance, which is why it works. – Aran-Fey Mar 24 '18 at 15:25
  • Yes, sorry, that comment wasn't helpful. `self` is never the `fish` class or an instance of the `fish` class. It's an instance of `catfish` or tunafish`, each of which have the required attribute. – Martijn Pieters Mar 24 '18 at 15:26

2 Answers2

4

You are accessing attributes on an instance, not on a class. Your self reference is never an instance of the fish class, only of one of the two derived classes, and those derived classes set the _colour attribute.

If you created an instance of fish() itself, you'd get an attribute error, because that instance will not have the attribute set.

You may perhaps think that in base classes, self becomes an instance of the base class; that's not the case.

Instead, attributes on an instance are looked up on the instance directly, and on its class and base classes. So self._colour looks at the instance, at type(instance) and at all further objects in the type(instance).__mro__, the Method Resolution Order that sets all classes in a hierarchy in a linear order.

You could print out the type() of your object:

>>> class fish:
...     def color(self):
...         print(type(self))
...         return self._colour
...
# your other class definitions
>>> print(catfish_obj.color())
<class '__main__.catfish'>
Blue Cat fish
>>> print(tunafish_obj.color())
<class '__main__.tunafish'>
Yellow Tuna fish

The self references are instances of the derived classes, passed into an inherited method. So self._colour will first look at the attributes directly set on self, then at type(self), and there _colour is found.

Perhaps it would help to see how Python methods work. Methods are just thin wrappers around functions, created when you look up the attribute on an instance:

>>> tunafish_obj.color  # access the method but not calling it
<bound method fish.color of <__main__.tunafish object at 0x110ba5278>>
>>> tunafish.color      # same attribute name, but on the class
<function fish.color at 0x110ba3510>
>>> tunafish.color.__get__(tunafish_obj, tunafish)  # what tunafish_obj.color actually does
<bound method fish.color of <__main__.tunafish object at 0x110ba5278>>
>>> tunafish_obj.color.__self__   # methods have attributes like __self__
<__main__.tunafish object at 0x110ba5278>
>>> tunafish_obj.color.__func__   # and __func__. Recognise the numbers?
<function fish.color at 0x110ba3510>

Look closely at the names of the objects I access, and what happens when I call the __get__ method on a function. Python uses a process called binding when you access certain attributes on an instance; when you access an attribute this way, and that points to an object with a __get__ method, then that object is called a descriptor, and __get__ is called to bind the object to whatever you looked the object up on. See the descriptor howto.

Accessing color on the instance, produces a bound method object, but the description of the object tells us it came from fish, it's named a *bound method fish.color of instance reference. Accessing the same name on the class gives us the fish.color function, and I can manually bind it to create a method again.

Finally, the method has an attribute __self__, which is the original instance, and __func__ which is the original function. And there's the magic, when you call a bound method, the method object just calls __func__(__self__, ....), so passing in the instance it was bound to.

When that function was inherited, (found on the fish class, so fish.color), it is still passed an instance of the derived class, and still has everything the derived class has.

Python is very dynamic, and very flexible. You can take any old function and put it on a class, and it can be bound into a method. Or you can take any unbound function, and manually pass in an object with the right attributes, and it'll just work. Python doesn't care, really. So you can pass in a new, indepdent type of object and still have the fish.color function work:

>>> fish.color  # original, unbound function on the base class
<function fish.color at 0x110ba3510>
>>> class FakeFish:
...     _colour = 'Fake!'
...
>>> fish.color(FakeFish)  # passing in a class! Uh-oh?
<class 'type'>
'Fake!'

So even passing in a class object, completely unrelated to the fish hierarchy, but with the expected attribute, still works.

To most Python code, if it walks like a duck, and quacks like a duck, the code will accept it as a duck. Call it duck typing.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
0

The methods of a derived class are not available in the base class. However, the fields are shared for any function operating on a particular object. self._colour refers to the value of _colour in the object on which you are calling color(), regardless of how _colour was set.

Edit because you are setting _colour = ... directly in the class, outside of a function, any catfish will have _colour == "Blue Cat fish" and any tunafish will have _colour == "Yellow Tuna fish". Those values, although set on the class, are available in every instance. That is why self._colour works even though you never directly said self._colour = .... If you wanted fish-specific colors, you would need to set self._colour in catfish.__init__ or tunafish.__init__.

cxw
  • 16,685
  • 2
  • 45
  • 81