Example code - Inheritance issues: Difference between revisions

From 22118
Jump to navigation Jump to search
mNo edit summary
 
(12 intermediate revisions by the same user not shown)
Line 12: Line 12:
             'feed': "is very excited" }
             'feed': "is very excited" }


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


     ### Magic methods
     ### Magic methods
Line 20: Line 20:
     def __add__(self, other):
     def __add__(self, other):
         # Taken a short cut and just demanding the genders to be different to breed
         # Taken a short cut and just demanding the genders to be different to breed
         if self.gender == other.gender:
         if self._gender == other._gender:
             return NotImplemented
             return NotImplemented
         # Being biased and saying all kittens are male
         # Being biased and saying all kittens are male
Line 37: Line 37:


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


     ### Instance methods
     ### Instance methods
Line 54: Line 54:
     def action(self, act):
     def action(self, act):
         if act in CatClass.acts:
         if act in CatClass.acts:
             print(self.name, CatClass.acts[act])
             print(self._name, CatClass.acts[act])
         elif self.gender == 'male':
         elif self._gender == 'male':
             print(self.name, "looks bored")
             print(self._name, "looks bored")
         else:
         else:
             print(self.name, "yawns")
             print(self._name, "yawns")


### Main program                     
### Main program                     
yourcat = CatClass('Missy', 'female')
yourcat = CatClass('Missy', 'female')
yourcat.age = 5
print(f"The age of your cat is {yourcat.age}")


CatClass.add_action("annoy", "scratches your face")
CatClass.add_action("annoy", "scratches your face")
Line 92: Line 90:
     def action(self, act):
     def action(self, act):
         if act in CatClass.acts:
         if act in CatClass.acts:
             print(self.name, CatClass.acts[act])
             print(self._name, CatClass.acts[act])
         elif self.gender == 'male':
         elif self._gender == 'male':
             print(self.name, "looks bored")
             print(self._name, "looks bored")
         else:
         else:
             print(self.name, "yawns")
             print(self._name, "yawns")


     # We are specifically using the actions in the CatClass for the behavior
     # We are specifically using the actions in the CatClass for the behavior
Line 103: Line 101:
     # These are the problematic lines, they can be replaced in different ways
     # These are the problematic lines, they can be replaced in different ways
         if act in CatClass.acts:
         if act in CatClass.acts:
             print(self.name, CatClass.acts[act])
             print(self._name, CatClass.acts[act])
     # Through '''self''', as self will search the instance namespace and then the class namespace for the variable
     # Through '''self''', as self will search the instance namespace and then the class namespace for the variable
         if act in self.acts:
         if act in self.acts:
             print(self.name, self.acts[act])
             print(self._name, self.acts[act])
     # By dynamically getting the class by type
     # By dynamically getting the class by type
         cls = type(self)
         cls = type(self)
         if act in cls.acts:
         if act in cls.acts:
             print(self.name, cls.acts[act])
             print(self._name, cls.acts[act])
</pre>
</pre>
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.
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.
Line 120: Line 118:
     }
     }


# By changing instead of replacing we might get somewhere good.
# Thats OK, we will just add the tiger actions later - we inherited the add_action from CatClass
class Tiger(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')
</pre>
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.
<pre>
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')
</pre>
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.....
<pre>
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')
</pre>
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)?<br>
The issue is in the ''__add__'' magic method. All that is required is that the two classes being "added" have a _gender instance variable.
<pre>
    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   
</pre>
Here are the two classes without any inheritance problems. I will leave it to you to do the testing.
<pre>
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['scratch'] = 'roars'
     acts['feed'] = 'demands more meat'
     acts['feed'] = 'demands more meat'
</pre>
</pre>
== 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.

Latest revision as of 20:22, 19 March 2026

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.