An Introduction To Python Classes – Inheritance, Encapsulation, and Polymorphism

This article continues from Introduction to Classes – Part One, where we explained what classes are, their components, and why we use them. We also looked at some unique characteristics of classes that assist us in creating cleaner code. If you haven’t read Part One and are new to classes, I suggest reading that introduction first.

*** Tutorial Part 1: Introduction to Python Classes ***

In today’s article, we’ll continue with the previous example of a grocery store, where we created a Stock class. With that class, we’ll explore the topic of inheritance, what that means and why we’d use it. We’ll also explain two other issues specific to classes, being encapsulation and polymorphism.

Inheritance – What Is It and Why Use It?

Inheritance between classes allows you to create a new class, inherit all the attributes and methods of an existing class while adding separate attributes and methods to the new class.

We use the analogy of Parent and Child. The Parent class is the one that gives the inheritance, and the Child class is the one that receives the inheritance. As in life, so in Python.

In Part One of this article, we used class Stock which allowed us to create grocery items with several attributes generic to all grocery items, namely a stock code, a description, a buy price, and a markup. There were also two methods: calculating the selling price and calculating a discounted price when passed the discount figure. Yet, different grocery items have different characteristics.

  • We measure can contents by volume in milliliters or mls;
  • We weigh meat in kilograms or kg.
  • Cans have a long shelf life; meat has a short use-by date.
  • We could try to add every possible grocery item variation into the class Stock, but that’s somewhat cumbersome.
  • How about keeping those generic items possessed by all grocery items in class Stock as a parent class, and create child classes for meat and cans respectively that cater to the specific needs of those items?

Here’s the code.

class Stock:
    category = 'Groceries'

    def __init__(self, stock_code, description, buy_price, mark_up):
        self.code = stock_code
        self.desc = description
        self.buy = buy_price
        self.margin = mark_up

    def sell_price(self):
        print('Retail price = $', round(self.buy * self.margin, 2))

    def sale(self, discount):
        print('The discounted price of {} is $'.format(C298.desc),
              round(self.buy * self.margin * (1- discount), 2))

class Canned(Stock):
    category = 'Cans'

    def __init__(self, stock_code, description, buy_price, mark_up, volume, manuf):
        Stock.__init__(self, stock_code, description, buy_price, mark_up)
        self.volume = volume
        self.manuf = manuf

    def multi_buy(self):
        print('Buy two {} of {} {} {} and get one free. Pay only ${}'.format(self.category, self.manuf, self.volume, self.desc, round(self.buy * self.margin, 2)))

C298 = Canned('C298', 'Chicken Soup', 0.75, 1.553, '400 mls', 'Campbells')

C298.sale(.15)

C298.multi_buy()

Let’s step through this. The Stock class code is the same as it was in the previous article. The addition is from the ‘class Canned(Stock):’  line of code. We’ve created a new class called Canned using the same syntax as we did with Stock; however, we’ve called Stock as the Parent, indicated by including it in the parentheses.

class Canned(Stock):
    category = 'Cans'

On the next line, we’ve created a class category of 'Cans', then we’ve used the __init__ function as before to define the parameters. Most of the parameters are the same as those in the Stock class, yet we’ve added two more, 'volume' and 'manuf'. These are the parameters specific to the Canned class. The following line uses Stock.__init__ to reference the Parent class parameters. This line is where the magic happens with inheritance. By calling class Canned(Stock) and inserting this line, you now have a link between the two classes that allows a transfer of attributes and methods.

    def __init__(self, stock_code, description, buy_price, mark_up, volume, manuf):
        Stock.__init__(self, stock_code, description, buy_price, mark_up)

We pass the new parameters 'volume' and 'manuf' to the self.volume and self.manuf attributes, then we’ve created a new method for the Canned class. This new method is called multi_buy(), and when activated, prints a label allowing shoppers to buy two cans of product for the price of one. 

        self.volume = volume
        self.manuf = manuf

    def multi_buy(self):
        print('Buy two {} of {} {} {} and get one free. Pay only ${}'.format(self.category, self.manuf, self.volume, self.desc, round(self.buy * self.margin, 2)))

The following line of code creates or ‘instantiates’ an object from class Canned using a stock code of C298 to create a can of Chicken Soup by passing the parameters in the required order.

C298 = Canned('C298', 'Chicken Soup', 0.75, 1.553, '400 mls', 'Campbells')

C298.sale(.15)

C298.multi_buy()

On the following line, we call the method sale() for our object and pass a 15% discount. Note that the sale() method belongs to the Stock() class, not the Canned class, yet it is accessible due to the inheritance flow between Child and Parent. We then call the new method we defined in the Canned class called multi_buy(). Here’s the result when we run the code.

# Result

The discounted price of Chicken Soup is $ 0.99
Buy two Cans of Campbells 400 mls Chicken Soup and get one free. Pay only $1.16

As you can see, we have the option of using the sale() method from the parent class Stock() or the multi_buy() method from the child class, Canned. Herein lies part of the magic of inheritance.

We can create as many child classes from Stock as we wish. Let’s create a class for meat. As we said before, we measure meat by weight and need to establish a use-by date as it’s a particularly perishable foodstuff. 

class Meat(Stock):
    category = 'Meat'

    def __init__(self, stock_code, description, buy_price, mark_up, weight, use_by):
        self.kilo = weight
        self.expiry = use_by
        Stock.__init__(self, stock_code, description, buy_price, mark_up)

    def Label(self):
        print(self.desc, '\nWeight: ', self.kilo, 'kgs', '\nExpiry: ', self.expiry)
        self.sell_price()

    def Expiring(self, discount):
        print('Price reduced for quick sale: ${}'.format(round(self.buy * self.margin * (1 - discount), 2)))

This code follows all of the steps we went through for the Canned class. We’ve created a class Meat(Stock), meaning it is a child of the Stock class. We’ve given it a category of Meat, then used the __init__ function to define the parameters we require. The two new ones that differ from the Stock class are ‘weight’ and ‘use_by’. We then pass those parameters to the self.kilo and self.expiry attributes. Finally, we use the Stock.__init__ command to create the link to the Parent parameters.

In Meat(), we’ve defined two methods specific to the Meat() class. The first is a method to print a label we can place on the outside of the meat packaging. The second is a discounting method that will reduce the meat in price as it comes close to its expiry date; we simply need to pass the discount to the method.

Now we’ll create, or instantiate, an object from the Meat() class for some Sirloin Steak we wish to sell in our shop, and we’ll call the two new methods, Label() and Expiring(). Then we’ll also call the multi_buy() method for the Chicken Soup to prove that the two objects of Sirloin Steak and Chicken Soup, created as Child classes of Parent class Stock(), can happily coexist.

C401 = Meat('C401', 'Sirloin Steak', 4.16, 1.654, .324, '15 June 2021')

C298 = Canned('C298', 'Chicken Soup', 0.75, 1.553, '400 mls', 'Campbells')

C401.Label()
print()
C401.Expiring(.35)
print()
C298.multi_buy()

# Result

Sirloin Steak 
Weight:  0.324 kgs 
Expiry:  15 June 2021
Retail price = $ 6.88

Price reduced for quick sale: $4.47

Buy two Cans of Campbells 400 mls Chicken Soup and get one free. Pay only $1.16

This example illustrates that we can create many children of a parent class, give each their own attributes and methods while also accessing the methods and attributes in the parent. Now let’s look at how encapsulation works in classes.

Encapsulation

Encapsulation is the ability, in object-oriented-programming, to limit modification to variables, attributes, or methods within a class. We’ll use the initial Stock class as an example to demonstrate this. Let’s assume we don’t wish to allow the ‘self.margin’ attribute to be easily modified. We can do this by using a single or double underscore in front of the attribute name.

In the following code, we’ll first show how easy it is to change the attribute.

class Stock:
    category = 'Groceries'

    def __init__(self, stock_code, description, buy_price, mark_up):
        self.code = stock_code
        self.desc = description
        self.buy = buy_price
        self.margin = mark_up

    def sell_price(self):
        print('Retail price = $', round(self.buy * self.margin, 2))

    def sale(self, discount):
        print('The discounted price of {} is $'.format(C298.desc),
              round(self.buy * self.margin * (1 - discount), 2))

C298 = Stock('C298', 'Chicken Soup', 0.75, 1.553)

C298.sell_price()

C298.margin = 1.2

C298.sell_price()

# Result

Retail price = $ 1.16
Retail price = $ 0.9

So by calling the margin attribute and applying a revised figure we can easily change the mark_up applied to our items. Now we’ll modify the code with double underscores in front of the attribute and try to modify it again.

class Stock:
    category = 'Groceries'

    def __init__(self, stock_code, description, buy_price, mark_up):
        self.code = stock_code
        self.desc = description
        self.buy = buy_price
        self.__margin = mark_up

    def sell_price(self):
        print('Retail price = $', round(self.buy * self.__margin, 2))

    def sale(self, discount):
        print('The discounted price of {} is $'.format(C298.desc),
              round(self.buy * self.__margin * (1 - discount), 2))

C298 = Stock('C298', 'Chicken Soup', 0.75, 1.553)

C298.sell_price()

C298.margin = 1.2

C298.sell_price()

# Result

Retail price = $ 1.16
Retail price = $ 1.16

So with the addition of double underscores in front of the margin attribute, we are now unable to modify the original figure easily. To do so requires a discrete method that will make the change when needed.

class Stock:
    category = 'Groceries'

    def __init__(self, stock_code, description, buy_price, mark_up):
        self.code = stock_code
        self.desc = description
        self.buy = buy_price
        self.__margin = mark_up

    def sell_price(self):
        print('Retail price = $', round(self.buy * self.__margin, 2))

    def sale(self, discount):
        print('The discounted price of {} is $'.format(C298.desc),
              round(self.buy * self.__margin * (1 - discount), 2))

    def setMargin(self, new_margin):
        self.__margin = new_margin

C298 = Stock('C298', 'Chicken Soup', 0.75, 1.553)

C298.sell_price()

C298.margin = 1.2

C298.sell_price()

C298.setMargin(1.426)

C298.sell_price()

# Result

Retail price = $ 1.16
Retail price = $ 1.16
Retail price = $ 1.07

With the new setMargin() method, we have now created a discrete means by which we can modify our sales margin. In the code above, we used the new method to alter the margin from 1.553 to 1.426, resulting in a reduced selling price of $1.07 per can.

Polymorphism

Polymorphism refers to something having many forms. In object-oriented programming, it refers to using the same function for different types. In classes, it means the function is indifferent to the type of class; as long as the methods exist, it will use it.

We’ll create a similar Label() method in our Canned class that we used in the Meat class to show this in action. The output of each method will be different, but the name of the method will be the same. Then we’ll create a function that will call the method Label() using the stock codes we have for the meat and the Soup. As you will see, polymorphism will allow both functions to operate independently to print out the correct labels.

class Stock:
    category = 'Groceries'

    …. # Code truncated for brevity

class Canned(Stock):
    category = 'Cans'

    def __init__(self, stock_code, description, buy_price, mark_up, volume, manuf):
        self.volume = volume
        self.manuf = manuf
        Stock.__init__(self, stock_code, description, buy_price, mark_up)

    def Label(self):
        print(self.desc, '\nVolume: ', self.volume)
        self.sell_price()

C298 = Canned('C298', 'Chicken Soup', 0.75, 1.553, '400 mls', 'Campbells')

class Meat(Stock):
    category = 'Meat'

    def __init__(self, stock_code, description, buy_price, mark_up, weight, use_by):
        self.kilo = weight
        self.expiry = use_by
        Stock.__init__(self, stock_code, description, buy_price, mark_up)

    def Label(self):
        print(self.desc, '\nWeight: ', self.kilo, 'kgs', '\nExpiry: ', self.expiry)
        self.sell_price()

C401 = Meat('C401', 'Sirloin Steak', 4.16, 1.654, .324, '15 June 2021')

def label_print(*args):
    for elem in args:
        elem.Label()
        print()

label_print(C401, C298)

# Result
Sirloin Steak 
Weight:  0.324 kgs 
Expiry:  15 June 2021
Retail price = $ 6.88

Chicken Soup 
Volume:  400 mls
Retail price = $ 1.16

As you can see in the previous code, the def Label(self): portion of the method is identical in each class, but the data to be printed to the label differs.

We then created a function outside all three classes called label_print(), and we allowed multiple arguments to be passed to it using the *args syntax in the brackets. Then we simply iterate through each argument, however many there may be, and call the Label() method in the applicable class to which that argument belongs. The result is we printed the labels for each object created from two different child classes.

Summary

In this second article on classes, we discussed the topic of inheritance, showing how we may create classes that have parent-child relationships, allowing the use of attributes and methods from a parent class to flow to a child.

We then discussed encapsulation, which restricts changes to variables, attributes, or methods within a class using the underscore or double underscore syntax. We showed how the use of a discrete method could make the desired change overtly.

Finally, we discussed polymorphism, which in our case uses one function to act on different methods, assuming that the method is in the class.

You can read part one of this article here:

*** Tutorial Part 1: Introduction to Python Classes ***

I trust these two articles have been helpful to understanding classes in Python. Thank you for reading.