Example code - Inheritance issues

From 22118
Revision as of 20:22, 19 March 2026 by WikiSysop (talk | contribs) (→‎Take home messages)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

The challenges in inheritance can not be demonstrated well in a powerpoint. Thus, I made this page with examples. You can copy the code in the section and add it together and run it.

First I create a slightly bigger CatClass.

class CatClass:
    ### Class variables
    genus = "Felis"
    species = "Catus"
    acts = {'scratch': "purrs",
            'scare': "jumps high in the air",
            'feed': "is very excited" }

    ### Instantiation
    def __init__(self, name, gender):
        self._name, self._gender = name, gender

    ### Magic methods
    # Addition makes a kitten if male and female cat, alternative constructor
    def __add__(self, other):
        # Taken a short cut and just demanding the genders to be different to breed
        if self._gender == other._gender:
            return NotImplemented
        # Being biased and saying all kittens are male
        kitten = CatClass('Unnamed kitten', 'male')
        return kitten     
        
    ### Class methods
    # Adding a cat action
    @classmethod
    def add_action(cls, action, reaction):
       cls.acts[action] = reaction
    # Querying class for number of actions
    @classmethod
    def count_action(cls):
        return len(cls.acts)

    ### Property methods
    # name setter and getter
    @property
    def name(self):
        if not hasattr(self, '_name'):
            raise NameError("Name of cat is not yet set")
        return self._name
        
    @name.setter
    def name(self, value):
        if value == '':
            raise ValueError("Name can not be nothing")
        self._name = value

    ### Instance methods
    # The action method - using class variables
    def action(self, act):
        if act in CatClass.acts:
            print(self._name, CatClass.acts[act])
        elif self._gender == 'male':
            print(self._name, "looks bored")
        else:
            print(self._name, "yawns")

### Main program                    
yourcat = CatClass('Missy', 'female')

CatClass.add_action("annoy", "scratches your face")
print("Actions in CatClass:", CatClass.count_action())

mycat = CatClass("Tom", "male")
kitten = mycat + yourcat

print(f"Kitten: {kitten.name}")

yourcat.action('scare')

This class looks fine, but it has a number of inheritance issues. By creating a Tiger class that inherits from the CatClass, I will demonstrate. Also, see how simple it was to create a new class based on an old :-)

class Tiger(CatClass):
    acts = {
        'scratch': 'roars',
        'feed': 'demands more meat'
    }

tigger = Tiger("Shere Khan", "male")
tigger.action("scratch")
tigger.action('scare')

We want the Tiger to react differently than the Cat, at least for some actions. However, this code does not do it. The problem lies in the inherited action method.

    def action(self, act):
        if act in CatClass.acts:
            print(self._name, CatClass.acts[act])
        elif self._gender == 'male':
            print(self._name, "looks bored")
        else:
            print(self._name, "yawns")

    # We are specifically using the actions in the CatClass for the behavior
    # We need to use the current class, as in cats uses CatClass actions and tigers uses Tiger actions.

    # These are the problematic lines, they can be replaced in different ways
        if act in CatClass.acts:
            print(self._name, CatClass.acts[act])
    # Through '''self''', as self will search the instance namespace and then the class namespace for the variable
        if act in self.acts:
            print(self._name, self.acts[act])
    # By dynamically getting the class by type
        cls = type(self)
        if act in cls.acts:
            print(self._name, cls.acts[act])

This does not completely solve the problem. We would still like to be able to scare the tiger (inherit the cat action), but for now it looks bored when we try. This problem is caused by the complete replacement of the class variable acts in the Tiger class. The cat actions are deleted and replaced with tiger-only actions.

class Tiger(CatClass):
    acts = {
        'scratch': 'roars',
        'feed': 'demands more meat'
    }

# Thats OK, we will just add the tiger actions later - we inherited the add_action from CatClass
class Tiger(CatClass):
    pass

tigger = Tiger("Shere Khan", "male")
tigger.add_action('scratch', 'roars')
tigger.add_action('feed', 'demands more meat')
# Testing
tigger.action('scare')
tigger.action('feed')
# All good, but what happened to our cat?
mycat.action('scratch')

The cat started roaring when scratched - that is a tiger action. Looking at the add_action class method, it is not specifying anywhere that the CatClass should be used so what is wrong? The problem is that the Tiger and the CatClass are sharing the class variables. The Tiger did not make new ones, so it is using the CatClass class variables, because CatClass is the parent class. Right now any changes in action made by the Tiger class is reflected in the CatClass and vice versa. The Tiger needs is own. Also did you notice that the Tiger's scientific name is Felis Catus, same as the cat - ridiculous.

class Tiger(CatClass):
    ### Class variables
    genus = "Panthera"
    species = "Tigris"
    acts = CatClass.acts.copy()
    # Tiger class specials
    acts['scratch'] = 'roars'
    acts['feed'} = 'demands more meat'

tigger = Tiger("Shere Khan", "male")
tigger.action('scratch')
mycat.action('scratch')

Much better. Copy the mutable class variables from the base class and then change them so they fit.

Life is good. Let's make some kittens.....

kitten1 = mycat + yourcat
print(kitten1.name)

# Can we mate the cat and the tiger?
kitten2 = yourcat + tigger
kitten2.name = "Monstrous Mutant"
print(kitten2.name)
kitten2.action('scratch')

That was unfortunate. We should not be able to breed a cat and a tiger, and if we did anyway, should the result be a cat (the kitten does a cat action)?
The issue is in the __add__ magic method. All that is required is that the two classes being "added" have a _gender instance variable.

    def __add__(self, other):
        # Taken a short cut and just demanding the genders to be different to breed
        if self._gender == other._gender:
            return NotImplemented
        # Being biased and saying all kittens are male
        kitten = CatClass('Unnamed kitten', 'male')
        return kitten

    # Adding a class check - we must demand that we breed tigers with tigers and cats with cats
    def __add__(self, other):
        if type(self) != type(other):
            return NotImplemented
        # Taken a short cut and just demanding the genders to be different to breed
        if self._gender == other._gender:
            return NotImplemented
        # Still male kittens, making sure tigers begets tigers and cats begets cats
        kitten = type(self)('Unnamed kitten', 'male')
        return kitten     

Here are the two classes without any inheritance problems. I will leave it to you to do the testing.

class CatClass:
    ### Class variables
    genus = "Felis"
    species = "Catus"
    acts = {'scratch': "purrs",
            'scare': "jumps high in the air",
            'feed': "is very excited" }

    ### Instantiation
    def __init__(self, name, gender):
        self._name, self._gender = name, gender

    ### Magic methods
    # Addition makes a kitten if male and female cat, alternative constructor
    # Adding a class check - we must demand that we breed tigers with tigers and cats with cats
    def __add__(self, other):
        if type(self) != type(other):
            return NotImplemented
        # Taken a short cut and just demanding the genders to be different to breed
        if self._gender == other._gender:
            return NotImplemented
        # Still male kittens, making sure tigers begets tigers and cats begets cats
        kitten = type(self)('Unnamed kitten', 'male')
        return kitten
                
    ### Class methods
    # Adding a cat action
    @classmethod
    def add_action(cls, action, reaction):
       cls.acts[action] = reaction
    # Querying class for number of actions
    @classmethod
    def count_action(cls):
        return len(cls.acts)

    ### Property methods
    # name setter and getter
    @property
    def name(self):
        if not hasattr(self, '_name'):
            raise NameError("Name of cat is not yet set")
        return self._name
        
    @name.setter
    def name(self, value):
        if value == '':
            raise ValueError("Name can not be nothing")
        self._name = value

    ### Instance methods
    # The action method - using class variables
    def action(self, act):
        if act in self.acts:
            print(self._name, self.acts[act])
        elif self._gender == 'male':
            print(self._name, "looks bored")
        else:
            print(self._name, "yawns")

class Tiger(CatClass):
    ### Class variables
    genus = "Panthera"
    species = "Tigris"
    acts = CatClass.acts.copy()
    # Tiger class specials
    acts['scratch'] = 'roars'
    acts['feed'] = 'demands more meat'

Take home messages

  • Even if you do not plan for your class to be inherited, make it so it can. That way it is less work later AND you remember how to do it.
  • Don't hardcode class names in your class. Use type(self) to get the class. This goes when using class variables, constructing new instances and elsewhere. But not when copying the parent class' class variables - there you have to name it :-)
  • Make a copy of mutable class variables if you have any plan or possibility to modify them.
  • In magic methods check that the other class is the same as self.
  • Notice that what you learned here is slightly different from what was told in the beginning of the powerpoint.