11  Classes

11.1 Object Oriented Programming

Python is what is called an object oriented programming language. This means that “objects” in Python represent both data and methods that operate on that data.

Let’s take a very simple example:

a = 1.5

When we do this we are not only binding the value 1.5 to a. We are also creating a floating-point object which comes with several methods. For example, one method it comes with is to check if the value is an integer:

a.is_integer()
False

In Python, a class is an object constructor. It is a blueprint for creating objects. By writing a class of our own, we can create objects that have a specific structure and methods.

11.2 Defining a Class

We will use a running example of a bank account to illustrate this concept. At its simplest level, a bank account has an account holder and a balance (the amount of money in the account).

We can create a simple class of a bank account as follows:

class BankAccount:

    def __init__(self, account_holder, opening_balance=0):
        self.account_holder = account_holder
        self.balance = opening_balance

The keyword class begins the class definition, followed by the class name followed with a :. The structure is like definining a function, but using class instead of def. By convention, class names use capital letters, so we write BankAccount rather than bank_account.

When defining a class, we always definte one function inside called __init__(). This initializes an object. The __init__ method runs automatically whenever an object of a certain class is created. In this example, whenever a new bank account is created. It sets the object’s initial data.

The parameter self refers to the particular object being created. The expressions self.account_holder and self.balance are the object’s attributes. They store information belonging to that account.

11.2.1 Creating Objects

Let’s create two objects with the class BankAccount:1

account1 = BankAccount("Ailill", 1005)
account2 = BankAccount("Medb", 1000)

We can see these have the class BankAccount:

type(account1)
__main__.BankAccount

We can access the name of the account holder for account 1 with:

account1.account_holder
'Ailill'

and their balance with:

account1.balance
1005

We can also adjust attributes of an object directly with:

account2.balance = 1100
account2.balance
1100

11.3 Adding Methods to a Class

We can easily add methods to a class by just defining functions under the initial __init__ function. We need to use self as the first argument for these functions. Let’s create deposit and withdraw methods which each take an amount as the argument:

class BankAccount:

    def __init__(self, account_holder, opening_balance=0):
        self.account_holder = account_holder
        self.balance = opening_balance

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
        else:
            print("Insufficient funds")

Because we have updated the class with these methods, we need to recreate our account objects so they get these methods:

account1 = BankAccount("Ailill", 1005)
account2 = BankAccount("Medb", 1000)

Let’s deposit 100 in Ailill’s bank account and check the balance:

account1.deposit(100)
account1.balance
1105

Notice that we didn’t need to provide something for self as an argument to deposit. The object we are applying the method to passes everything in self to the method (in this case, the account holder and the balance).

Now let’s try withdraw 600 twice from Medb’s bank account:

account2.withdraw(600)
account2.balance
400
account2.withdraw(600)
account2.balance
Insufficient funds
400

11.4 Inheritance

The bank account we developed is just a standard current account which can do deposits and withdraws. Suppose we wanted to also create a savings account which can generate interest. We don’t have to create a new class from scratch, but rather we can create a new class that inherits the methods of the standard bank account and add only whatever extra functionality we need.

11.4.1 Savings Account

Let’s create a new class called SavingsAccount doing this:

class SavingsAccount(BankAccount):

    def add_interest(self, rate):
        self.balance *= (1 + rate / 100)

We can create a new account using the SavingsAccount in a similar way to BankAccount, with the account holder and initial balance:

account3 = SavingsAccount("Ailill", 500)

We can use all the methods from the standard BankAccount class, even though we didn’t need to redefine them here:

account3.deposit(500)
account3.balance
1000

And we can use the additional functionality we added (applying interest). Let’s apply 1% interest:

account3.add_interest(1)
account3.balance
1010.0

11.4.2 Checking Account

We can also create an new class inheriting methods from another, and overriding some of its methods. In addition, we can add additional arguments to this class.

For example, suppose we have a checking account that charges a fee for each withdrawal. The fee is a new argument when creating the function.

class CheckingAccount(BankAccount):

    def __init__(self, owner, balance, fee):
        super().__init__(owner, balance)
        self.fee = fee

    def withdraw(self, amount):
        super().withdraw(amount + self.fee)

Here the super() function gives the class access to methods and properties of the parent class (BankAccount). In __init__() we didn’t need to redefine it completely. The line super().__init__(owner, balance) runs the __init() function from the parent class and sets:

self.account_holder = account_holder
self.balance = opening_balance

We then only need to do the new step of setting self.fee = fee.

In the new withdraw function, we also use super(). Here we essentially re-use the withdraw() function from the parent class, but add the fee to it. It is equivalent to replacing it with the function:

    def withdraw(self, amount):
        if amount + self.fee <= self.balance:
            self.balance -= amount + self.fee
        else:
            print("Insufficient funds")

But notice that this is just the original withdraw() function with amount replaced with amount + self.fee. So we can just reuse our old function and just add the fee to the amount. This is what super() allows us to do.

Let’s test it out with fee = 1:

account4 = CheckingAccount("Medb", 500, 1)
account4.deposit(200)
account4.balance
700
account4.withdraw(100)
account4.balance
599

The fee was applied! Notice that for bank accounts with the parent class BankAccount the fee is not applied:

account2.balance
400
account2.withdraw(100)
account2.balance
300

11.5 Key Terms

Now that we have seen examples of these various things, it’s time to recap all the terminology we have just seen:

  • A class is an object constructor. It is blueprint for creating objects. Here, BankAccount was a class.
  • An object is a specific item created from a class. Here account1 is an object created from the class BankAccount.
  • An attribute stores information about an object. Here, account holder and balance are attributes of a bank account.
  • A method defines an action that an object can perform. Here, deposit and withdraw were two methods we created.
  • The __init__ method sets the initial state of a new object.
  • The self parameter refers to the object on which a method is operating.

  1. These two names are characters from the Irish mythological story the Táin Bó Cúailnge. King Ailill and Queen Medb had a quarrel over who had the most wealth (i.e. money in their bank account).↩︎