Classes, Objects, and Inheritance

In today’s lecture, we will continue our discussion of object oriented programming in Python.

Example: Name class

If our class represents names, we can choose how these names are printed.

class Name:
    """Class to represent a person's name."""
    __slots__ = ['_f', '_m', '_l']
    
    def __init__(self, first, last, middle=''):
        print("__init__ is running")
        self._f = first
        self._m = middle
        self._l = last

    def getFirst(self):
        return self._f
    
    def __str__(self):
        print("in __str__")
        # if the person has a middle name
        if len(self._m):  
            return '{}. {}. {}'.format(self._f[0], self._m[0], self._l) 
        else:
            return '{}. {}'.format(self._f[0], self._l)
n1 = Name('Rohit', 'Bhattacharya')
n2 = Name('Jeannie', 'Albrecht', 'Raye')
__init__ is running
__init__ is running
print(n1)
print(n2)
in __str__
R. Bhattacharya
in __str__
J. R. Albrecht
n1.__str__()
in __str__
'R. Bhattacharya'

Defining a method for initials

Now let’s implement a method for generating someone’s initials.

class Name:
    """Class to represent a person's name."""
    __slots__ = ['_f', '_m', '_l']
    
    def __init__(self, first, last, middle=''):
        self._f = first
        self._m = middle
        self._l = last
        
    def initials(self):
        if len(self._m):
            return '{}. {}. {}.'.format(self._f[0], self._m[0], self._l[0]).upper()
        else:
            return '{}. {}.'.format(self._f[0], self._l[0]).upper()

    def __str__(self):
        # if the person has a middle name
        if len(self._m):  
            return '{}. {}. {}'.format(self._f[0], self._m[0], self._l) 
        else:
            return '{}. {}'.format(self._f[0], self._l)
        
n1 = Name('Steve', 'Freund', 'N')
n1.initials()
'S. N. F.'
n2 = Name('Lida', 'Doret', 'P')
n2.initials()
'L. P. D.'

Inheritance

Inheritance is the capability of one class to derive or inherit the properties from another class. The benefits of inheritance are:

  • Often represents real-world relationships well

  • Provides reusability of code, so we don’t have to write the same code again and again

  • Allows us to add more features to a class without modifying it

Inheritance is transitive in nature, which means that if class B inherits from class A, then all the subclasses of B would also automatically inherit from class A.

When a class inherits from another class, all methods and attributes are accessible to subclass, except private attributes (indicated with __)

Person example

Suppose we want to define classes to represent all people on campus.

We can start with a parent or “super” class that defines the attributes common across all groups of people.

class Person:
    __slots__ = ['_name']
    
    def __init__(self, name):
        self._name = name
        
    def getName(self):
        return self._name
    
    def __str__(self):
        return self._name

Now suppose we define three subclasses that all inherit from Person but also add additional functionality: Student, Faculty, Staff.

Students inherit from the Person class, but also add attributes and getter methods for year and major.

class Student(Person):
    __slots__ = ['_year', '_major']
    
    def __init__(self, name, year, major):
        # call __init__ of Person (the super class)
        super().__init__(name)  
        self._year = year
        self._major = major
    
    def getYear(self):
        return self._year

    def getMajor(self):
        return self._major
    
    def setMajor(self, major):
        self._major = major
    
    def __str__(self):
        return "{}, {}, {}".format(self._name, self._major, self._year)

Faculty also inherits from Person, but unlike Student, it does not define attributes for year and major since that does not apply to Faculty. Instead, the Faculty class defines attributes for dept and office.

class Faculty(Person):
    __slots__ = ['_dept', '_office']
    
    def __init__(self, name, dept, office):
        # call __init__ of Person (the super class)
        super().__init__(name)  
        self._dept = dept
        self._office = office
    
    def getDept(self):
        return self._dept

    def getOffice(self):
        return self._office
    

Finally, the Staff class also inherits from Person. It adds an attribute for fulltime. Note the use of the getStatus() getter method to return a string even though _fulltime is being stored as a boolean attribute.

class Staff(Person):
    # fulltime is a Boolean 
    __slots__ = ['_fulltime']
    
    def __init__(self, name, fulltime):
        # call __init__ of super class
        super().__init__(name)  
        self._fulltime = fulltime
    
    def getStatus(self):
        if self._fulltime: 
            return "fulltime"
        return "partime"
    
    def __str__(self):
        return "{}, {}".format(self.getName(), self.getStatus())

Now let’s try using our classes.

jane = Student("Jane", 2024, "CS")
# inherited from Person
jane.getName()    
'Jane'
type(jane)
__main__.Student
# defined in Student
jane.getMajor()
'CS'
jane.setMajor("Math")
jane.getMajor()
'Math'
print(jane)
Jane, Math, 2024
rohit = Faculty("Rohit", "CS", "TBL 309B")
rohit.getName()
'Rohit'
rohit.getDept()
'CS'
print(rohit)
Rohit
# this doesn't work since instances of Faculty do 
# not have a major attribute
rohit.getMajor()  
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/var/folders/md/kwd9nc_d2ns0hw9wsvdrnt2c0000gn/T/ipykernel_55965/2826432291.py in <module>
      1 # this doesn't work since instances of Faculty do
      2 # not have a major attribute
----> 3 rohit.getMajor()

AttributeError: 'Faculty' object has no attribute 'getMajor'
fred = Staff("Fred", False)
print(fred)
Fred, partime
fred.getStatus()
'partime'