Main Menu: PYTHON PROGRAMMING
Doing it with Class
You learned how to use loops and statements in Python. You've worked with functions and understood namespaces and scopes. It is time to step into the world of object-oriented programming - known simply as OOP. In this chapter I am going to show you how to create your own custom classes in Python and declare objects for each of these classes. Learn how to define your own class variables and methods and access these to manipulate the objects you create for the class. Using these concepts, we will develop a program to help you manage your shared expenses with your friends, before stepping into the larger world of object oriented programming in Python.
6.1: The Class System
NOTE: Before you start, remember to Ceate a New File in IDLE Editor!

Hey people. How are you doing? We've completed five whole chapters, and I really hope you've been enjoying working with Python. In the previous chapter we learned about functions in and how we can organize our code by defining functions, passing data into function, and returning data from functions. In this chapter we're going to take another step towards writing modular code: Classes in Python.

A class is nothing but a template that defines the attributes and behaviour of objects. What are objects? Any singular data value or data-structure or collection created in your Python program is an object. Take for example the following code snippet:


>>> # Declare a variable "a"
>>> a = 10
>>> # Check the class of the variable  
>>> type(a)
<class 'int'>
>>>              
            

In the above snippet, we initialized a variable 'a' with a value 10. The variable 'a' belongs to the class 'int' as seen when passed into the type( ) function.

Let's take a few more examples of objects:


>>> # Declare a float variable "floatVariable"
>>> floatVariable = 5.3
>>> type(floatVariable)
<class 'float'>

>>> # Declare a string variable "stringVariable"
>>> stringVariable = "Kevin Sequeira"
>>> type(stringVariable)
<class 'str'>

>>> # Declare a list "listObject"
>>> listObject = ["Python", "is", "fun"]
>>> type(listObject)
<class 'list'>

>>> # Declare a dictionary "dictObject"
>>> dictObject = {"State": "South Australia", "PIN": 5073}
>>> type(dictObject)
<class 'dict'>

>>> # What about values not assigned to a variable? For example
>>> type(20)
<class 'int'>
>>> type("Annet")
<class 'str'>
>>> type(("Diana", [100, 200, 300]))
<class 'tuple'>
            

From the examples in the above snippet, we see that objects of different data-types in Python belong to different classes. As a definition, an object is an instance of a class. And depending on the class they belong to, objects behave differently. Take for example the objects in the snippet below:


>>> # Declare an integer and a string variable:
>>> intVariable = 10
>>> strVariable = "Shazam"
>>> 
>>> # You can carry out arithmetic operations on "intVariable" and not "strVariable"
>>> intVariable / 2
5.0
>>> strVariable / 2
Traceback (most recent call last):
  File "<pyshell#25>", line 1, in <module>
    strVariable / 2
TypeError: unsupported operand type(s) for /: 'str' and 'int'
>>> 
>>> # In the same way, you cannot index "intVariable" but you can index "strVariable"
>>> strVariable[4]
'a'
>>> intVariable[4]
Traceback (most recent call last):
  File "<pyshell#30>", line 1, in <module>
    intVariable[4]
TypeError: 'int' object is not subscriptable
            

Thus, the class that an object belongs to determines the behaviour of the object. If you still need help understanding, let's liken these objects to a a pekinese dog and a great white shark.

The pekinese dog belongs to the class mammals. It has certain attributes that are unique to its class, such as having four legs and the ability to breathe outside water. There are also certain actions and behaviour unique to mammals that the pekinese dog can do, but the great white shark cannot, such as barking at strangers and scratching tree trunks with their front legs.

Similarly, the great white shark has attributes unique to the class fishes. It doesn't have legs, but fins that allow it to move. Also, it can breathe underwater, but not outside water like the pekinese dog.

That's basically how classes work in Python. They act as templates for objects.

One of the things that makes Python special is that everything in Python is an object, making Python an object-oriented programming language. This means Python's features revolve around behaviour of objects, and allow you to deal with problems by creating objects with specific attributes and functions.

In other words, you can define your own, custom class in Python, and instantiate objects belonging to your custom class.
6.2: Defining a Class
Defining your own class in Python is really easy. Let's start with an example right away. Open IDLE (Python 3.7), go to File > New File. A new file will open in the IDLE Editor. Start typing out the code given below:


# Let's define a class "student"
# NOTE: A class definition starts with the keyword "class"
class student:
    def __init__(self):
        self.firstName = ""
        self.lastName = ""
    
    def setFirstName(self, firstName):
        self.firstName = firstName
    
    def setLastName(self, lastName):
        self.lastName = lastName

    def getFirstName(self):
        print("First Name:", self.firstName)

    def getLastName(self):
        print("Last Name:", self.lastName)
    
# Here is where the "main section" of the program starts
# Start with creating an instance of the class "student"
# Let's call this instance "student001"
student001 = student()

# Let's call the function "setFirstName"
# This function assigns a value to the variable "firstName"
student001.setFirstName("Diana")

# Let's call the function "setLastName"
# This function assigns a value to the variable "lastName"
student001.setLastName("Sequeira")

# Calling the function "getFirstName" prints
# the first name assigned to the object "student001"
student001.getFirstName()

# Similarly, calling the function "getLastName" prints
# the last name assigned to the object "student001"
student001.getLastName()
            

After typing out the code in the IDLE Editor, save the code in D:/Python Programming as myFirstClass.py. Once the code is saved, press F5 or go to Run > Run Module. You should see the folloing output on your screen:


First Name: Diana
Last Name: Sequeira
>>>                 
            

The output is straightforward, so let's get to understanding how the program works.

We started with first defining a class using the class keyword. Pay attention to the five functions defined inside the class 'student'. Each of these functions, defined inside a class, are called 'methods'.

Recall, in the previous section of this chapter, we said a class is a template that defines the attributes and behaviour of objects. The __init__( ) method defined at the start of the class is used to initialize any attributes belonging to the class. What are attributes? Attributes are nothing but variables that belong to a class - in this case the variables 'firstName' and 'lastName' initialized as empty strings in the method __init__( ).

NOTE: The __init__( ) method is the first method to be called and by default when we instantiate an object of the class. This method is called a 'constructor' as it initializes or constructs the object of the class. It must always be defined inside a class and be named __init__( ).

Notice the self keyword as a paramter inside the __init__( ) function definition? This is an important keyword. Naturally, when you instantiate more than one obect of a class, the objects will contain variables of the same name but with different values. The use of the self keyword allows you to locally initialize and manipulate variables for each object. When we created the object student001 = student(), Python called the function __init__( ) and initialized the variables 'firstName' and 'lastName' as empty strings using the self keyword.

Don't worry if this is slightly confusing. You'll get a hang of it as we develop more programs using classes.

The rest of the methods defined inside the class 'student' are pretty simple to understand.

Let's now focus on the 'main program' section of the code, written outside the classstudent. In the first line, we created an object 'student001' as an instance of the class 'student'. We then call the setFirstName( ) method for the object 'student001'. Note the syntax for calling the method: student001.setFirstName("Diana"). Methods for an object must always be called in this manner.

In the same manner, we set last name for the object 'student001' by calling the function setLastName( ) as student001.setLastName("Sequeira"). Next, we call the functions getFirstName( ) and getLastName( ) which returns the values of 'firstName' and 'lastName' assigned to the object 'student001'.
6.3: Initializing Attributes with Constructors
So, in the previous section you learned how to create a class and instantiate an object for the class. You also learned how Python initializes attributes of a class for each object created using the __init__( ) method, also known as the constructor method. In this section, let's look at different ways of initializing attributes with values during object creation using constructors.

Open IDLE (Python 3.7), go to File > New File. A new file will open in the IDLE Editor. Start typing out the code given below:


# Define a class "student" as before
# This class will be very similar to the one in the previous program,
# but with slight changes to the "__init__( )" method
class student:
    def __init__(self, firstNameParameter, lastNameParameter):
        self.firstName = firstNameParameter
        self.lastName = lastNameParameter

    def getFullName(self):
        return self.firstName + " " + self.lastName

# Here is where the "main section" of the program starts
# Start with creating an instance of the class "student"
# Let's call this instance "student001"
student001 = student("Annet", "D'Souza")
# Alright! Let's create another instance and call it "student002"
student002 = student("Diana", "Sequeira")

# Calling the function "getFullName" returns a string
# containing the first and last name assigned to an object of the class "student"
print("Full name of Student 001:", student001.getFullName())
print("Full name of Student 002:", student002.getFullName())
            

Save this code in D:/Python Programming as initializingWithConstructors.py. I know, such long names... Anyway, after you've saved the file, hit F5 or go to Run > Run Module. This should be your output:


Full name of Student 001: Annet D'Souza
Full name of Student 002: Diana Sequeira
>>>                
            

It's really simple to understand what's happening here. Let's start with reviewing the code written for the class 'student'.

The __init__( ) constructor method created for the class 'student' accepts two arguments when it is called. These arguments are represented by the parameters 'firstNameParameter' and 'lastNameParameter' in the constructor definition. The values passed into the constructor are then used to initialize two class attributes 'firstName' and 'lastName'.

Inside the class 'student', we also defined a method getFullName( ). This method returns a string created by concatenating the attributes 'firstName' and 'lastName' with a blank space in between.

Come to the "main program" section of the code. We start with creating two objects of the class 'student'. NOTE: In this previous program (myFirstClass.py), we simply created an object without passing any data into the constructor. Instead, we created a separate method for passing data into the class and initializing attributes. However this time we passed data into the class constructor, initializing the attributes at the moment of object creation. The use of constructors help with quicker and cleaner attribute initialization.

Finally, we called the getFullName( ) method with reference to each object. The string returned from the methods shows that while both objects belong to the same class and thus have the same attribute names, the values assigned to them are different, with reference to the object itself.
6.4: Accessing Object Attributes outside a Class
Alright. So I believe you now have a good understanding of how to define a class, create objects of a class and manipulate object attributes using class methods. In both programs we wrote previously, we used class methods to initialize attributes, set attribute values and get the attribute values. While this is a good practice, it is not always necessary. Python allows you to set and get attribute values by directly accessing these attributes outside of the class. Reopen the code we just wrote (initializingWithConstructors.py) and add these lines below the last line:


# Create a new object "student003"
# Don't forget to pass arguments for "firstName" and "lastName"
student003 = student("Kevin", "Sequeira")

# Let's print "student003's" full name without calling
# the function "getFullName"
print("Full name of Student 003:", student003.firstName + " " + student003.lastName)

# How about we change "student003's" first name
# and print the full name again, this time by calling the function
# "getFullName"
student003.firstName = "Panda"
print("New full name of Student 003:", student003.getFullName())
            

Save the code, and then hit F5 or go to Run > Run Module. This should give you the following output:


Full name of Student 001: Annet D'Souza
Full name of Student 002: Diana Sequeira
Full name of Student 003: Kevin Sequeira
New full name of Student 003: Panda Sequeira                 
            

Let's review the new piece of code we wrote. The first two lines of the output have already been explained in the previous section when we wrote the code for initializingWithConstructors.py. The last two lines of code are produced by the new piece of code we just added to the program.

We created a new object 'student003' and initialized the attributes 'firstName' and 'lastName' using the constructor method. After that, we printed the full name assigned to the object 'student003', however, we did not use the getFullName( ) method as used with the first two objects. Instead we directly fetched the values or the attributes 'firstName' and 'lastName' using the syntax objectName.attributeName. In this way, student003.firstName and student003.lastName give us the values of the attributes assigned to 'student003.

Next, we change the value of the variable 'firstName' assigned to the object 'student003' by directly assigning a new value to it using student003.firstName = "Panda". To verify that a new value has been successfully assigned to the attribute, we print the full name for 'student003' using the function getFullName( ). The string returned by the function shows that the value of 'firstName' was successfully changed for object 'student003'.
6.5: An Example: An Expense-Sharing Application using Classes
So far you've learned to create classes, initialize and access class attributes through objects and call class methods for different objects. Which means you are now familiar with the basics of classes and modular programming. Shall we put what we know to the test?

Let's develop a simple Python program that assists you with your expenses shared between friends. The program will take the following inputs:
  1. Names of friends who will be sharing expenses
  2. Name of each item that has been purchased
  3. Price of each item that has been purchase
  4. Names of friends who will be using each item
  5. Name of the person who paid for the item
Let's start with writing the code then. Open a new file in IDLE Editor and start typing the code below:


# We'll start by defining the class "personDetails" which will hold
# information about each person who will be sharing expenses.
class personDetails:
    def __init__(self, personName):
        self.personName = personName
        self.friendsDict = {}
        self.itemsDict = {}

    def setItemsPurchased(self, itemName, itemQuantity, itemPrice, sharedWith):
        if itemName in self.itemsDict.keys():
            self.itemsDict[itemName]["Item Quantity"] = self.itemsDict[itemName]["Item Quantity"] + itemQuantity
            self.itemsDict[itemName]["Total Price"] = self.itemsDict[itemName]["Total Price"] + itemPrice
            self.itemsDict[itemName]["Shared With"] = self.itemsDict[itemName]["Shared With"] | sharedWith 
        else:
            self.itemsDict[itemName] = {"Item Quantity": itemQuantity,
                                        "Total Price": itemPrice,
                                        "Shared With": sharedWith}

    def setUnsettledExpenses(self, friendName, itemName, moneyOwed):
        if friendName in self.friendsDict.keys():
            self.friendsDict[friendName]["Total Unsettled"] = self.friendsDict[friendName]["Total Unsettled"] + moneyOwed
            if itemName in self.friendsDict[friendName]["Item Details"].keys():
                self.friendsDict[friendName]["Item Details"][itemName] = self.friendsDict[friendName]["Item Details"][itemName] + moneyOwed
            else:
                self.friendsDict[friendName]["Item Details"][itemName] = moneyOwed
        else:
            self.friendsDict[friendName] = {"Total Unsettled": moneyOwed,
                                            "Item Details": {itemName: moneyOwed}}
            

Shall we review the code we just wrote? We defined a class personDetails. This class contains an __init__( ) constructor and two more functions: setItemsPurchased( ) and setUnsettledExpenses( ).

The constructor is run by default when a new instance is created for the class personDetails. It accepts the name of the person during instantiation and stores it in the variable 'personName'. It also initializes two dictionaries, 'friendsDict' and 'itemsDict' which store data about those friends with whom the person shares items, and about those items which he shares with friends, respectively.

The function setItemsPurchased( ) counts the total number of units of each item purchased by a person, calculates the total price paid for all units, stores the names of friends with whom the items were shared. All of this data is stored in the dictionary 'itemsDict' which is initialized in the constructor when the object is instantiated. The dictionary uses the item names as the key, and a nested dictionary which holds information for item quantity, total price and list of friends with whom the item is shared.

The function setUnsettledExpenses( ) calculates the initial unsettled expenses that the person owes to each friend for each item shared with them. This data is stored in the dictionary 'friendsDict', which contains the friend names as keys. Each key is linked to a nested dictionary which contains one key for the total unsettled money owed to the friend, and another dictionary for money owed for each item.

The next step is to write the code that minimizes the transaction between each friend by taking into account all expenses by different sets of people within the group. Type out the following code below the code for the class that you wrote above...


# We'll start by defining the class "personDetails"...
class personDetails:
    def __init__(self, personName):
        ...
    
    def setItemsPurchased(self, itemName, itemQuantity, itemPrice, sharedWith):
        ...

    def setUnsettledExpenses(self, friendName, itemName, moneyOwed):
        ...

# Ignore the grayed out code before this point. It is only for your reference to know
# where to start writing new code...
# Next, define the function to create objects for each person
def createPersonObjects():
    print("Please enter names of people who will be sharing expenses:")
    friendObjects = {}
    friendName = input("Add a person: ")
    friendObjects[friendName] = personDetails(friendName)
    friendName = input("Add another person: ")
    friendObjects[friendName] = personDetails(friendName)
    while True:
        friendName = input("Add another name or enter 'DONE': ")
        if friendName.lower() == "done":
            break
        else:
            friendObjects[friendName] = personDetails(friendName)
    print("\n" + "That's great! Start adding the items purchased" + "\n")
    return friendObjects

# Here, we start writing the "main code" for the program
# All functions must be written in order of compilation before this line
friendObjects = createPersonObjects()
            

The function createPersonObjects( ) carries out a simple operation. It creates objects of the class personDetails for each person. However, we do not know the number of friends who will be sharing expenses before we complete writing our code, which means every time we run the code, it will need to create a different number of objects. To handle this, we need to write code that can accept and store different number of objects each time the program is run.

When the function is called it creates an empty dictionary 'friendObjects'. Next, it accepts the name of the first friend, stores it in the variable 'friendName', and intantiates an object. This object is stored in the dictionary, where the value of 'friendName' becomes the key, and the object created becomes the value in the key-value pair.

The function carries out the above process for a second friend, saving the object as a new key-value pair. It then runs a while loop to check if the user wants to add any more persons to the list of objects. If the person enters "DONE" instead of a name, the program control breaks out of the loop and returns the dictionary of objects to the code block where the function was called.

Now that we've written code for creating each objects, which will be called in the first line of the main code friendObjects = createPersonObjects(), let's write the code for adding items that will be shared between friends. Type out the following code between the function createPersonObjects( ) and the "main code" section in your Python file...


# We'll start by defining the class "personDetails"...
class personDetails:
    def __init__(self, personName):
        ...
        ...

def createPersonObjects():
...
    
# Ignore the grayed out code before this point. It is only for your reference to know
# where to start writing new code...
# Define a function for splitting the expense of each itme between a list of friends.
def splitExpense(personObjects, itemName, sharedByList, itemPrice, paidBy):
    amountOwed = itemPrice / (len(sharedByList) + 1)
    for sharedBy in sharedByList:
        personObjects[sharedBy].setUnsettledExpenses(paidBy, itemName, amountOwed)
    return personObjects

# Next, define the function for adding shared items
def addItemSharing(personObjects):
    while True:
        itemName = input("Item Name: ")
        itemPrice = float(input("Item Price: "))
        itemQuantity = float(input("Item Quantity: "))
        paidBy = input("Purchased By: ")
        sharedByList = []
        itemSharedBy = input("Shared by (add a name): ")
        sharedByList.append(itemSharedBy)
        while True:
            itemSharedBy = input("Add another name or enter 'DONE': ")
            if itemSharedBy.lower() == "done":
                break
            else:
                sharedByList.append(itemSharedBy)
        personObjects[paidBy].setItemsPurchased(itemName, itemQuantity, itemPrice, itemSharedBy)
        personObjects = splitExpense(personObjects, itemName, sharedByList, itemPrice, paidBy)
        print(" ")
        print("Would you like to add more items? ['YES' / 'NO']: ")
        choice = input("Input 'YES' or 'NO': ")
        if choice.lower() == "no":
            print(" ")
            break
    return personObjects   

# Here, we start writing the "main code" for the program
# All functions must be written in order of compilation before this line
friendObjects = createPersonObjects()
friendObjects = addItemSharing(friendObjects)
            

The function addItemSharing( ) accepts the following information: name of the item purchased by the user, price of the item purchased by the user, quantity of the item purchased by the user and the list of people who will share it.

The function body is made up of two while loops. The outer while loop accepts the name, purchases and quantity of each item, while the nested while loop accepts the list of people who will be sharing the item.

For each item, after the list of people sharing the item is recorded, the program splits the expense for the item using the splitExpense( ) function defined above addItemSharing( ).

The addItemSharing( ) function is called in the second line of the main code friendObjects = addItemSharing(friendObjects). The line passes the dictionary of objects into the function and the returned dictionary is stored in a new dictionary variable of the same name.

The next step is to minimize the amount owed between all friends so that each person pays only what is necessary to others. For that we'll require the following code:


# Define a function for splitting the expense of each itme between a list of friends.
def splitExpense(personObjects, itemName, sharedByList, itemPrice, paidBy):
    ...

# Next, define the function for adding shared items
def addItemSharing(personObjects):
    ...    

# Define a function for minimizing the amount owed between each friend...
def optimizePayments(personObjects):
    personList01 = list(personObjects.keys())
    personList02 = list(personObjects.keys())
    for person01 in personList01:
        for person02 in personList02:
            if person01 == person02:
                continue
            elif ((person01 not in personObjects[person02].friendsDict.keys()) or (person02 not in personObjects[person01].friendsDict.keys())):
                continue
            else:
                difference = personObjects[person01].friendsDict[person02]["Total Unsettled"] - personObjects[person02].friendsDict[person01]["Total Unsettled"]
                if difference < 0:
                    personObjects[person01].friendsDict[person02]["Total Unsettled"] = 0
                    personObjects[person02].friendsDict[person01]["Total Unsettled"] = abs(difference)
                elif difference > 0:
                    personObjects[person01].friendsDict[person02]["Total Unsettled"] = difference
                    personObjects[person02].friendsDict[person01]["Total Unsettled"] = 0
                elif difference == 0:
                    personObjects[person01].friendsDict[person02]["Total Unsettled"] = 0
                    personObjects[person02].friendsDict[person01]["Total Unsettled"] = 0
        personList02.remove(person01)
    return personObjects

# Define a function to print the final unsettled amount between each friend.
def getFinalUnsettled(personObjects):
    for person01 in personObjects.keys():
        print(person01 + " has to pay...")
        for person02 in personObjects.keys():
            if person01 == person02:
                continue
            elif (person02 not in personObjects[person01].friendsDict.keys()):
                continue
            else:
                print(personObjects[person01].friendsDict[person02]["Total Unsettled"], "to", person02)
        print(" ")

# Here, we start writing the "main code" for the program
# All functions must be written in order of compilation before this line
friendObjects = createPersonObjects()
friendObjects = addItemSharing(friendObjects)
friendObjects = optimizePayments(friendObjects)
getFinalUnsettled(friendObjects)
            

The line friendObjects = optimizePayments(friendObjects) in the main code calls the optimizePayments( ) function. This function checks the total unsettled expense two friends owe to each other, and then calculates the difference between the two until it cannot be further minimized.

The line getFinalUnsettled(friendObjects) returns the calculated unsettled payments that friends owe to each other.

Save the code in D:/Python Programming as ourSharedExpenses.py, and then hit F5 or go to Run > Run Module. This should be your output:


Please enter names of people who will be sharing expenses:
Add a person: Kevin
Add another person: Abhinava
Add another name or enter 'DONE': Bipin
Add another name or enter 'DONE': Divyesh
Add another name or enter 'DONE': done

That's great! Start adding the items purchased

Item Name: Banana
Item Price: 3.2
Item Quantity: 1
Purchased By: Divyesh
Shared by (add a name): Abhinava
Add another name or enter 'DONE': Bipin
Add another name or enter 'DONE': done
 
Would you like to add more items? ['YES' / 'NO']: 
Input 'YES' or 'NO': yes
Item Name: Eggs
Item Price: 5
Item Quantity: 24
Purchased By: Bipin
Shared by (add a name): Abhinava
Add another name or enter 'DONE': Kevin
Add another name or enter 'DONE': done
 
Would you like to add more items? ['YES' / 'NO']: 
Input 'YES' or 'NO': no
 
Kevin has to pay...
1.6666666666666667 to Bipin
 
Abhinava has to pay...
1.6666666666666667 to Bipin
1.0666666666666667 to Divyesh
 
Bipin has to pay...
1.0666666666666667 to Divyesh
 
Divyesh has to pay...
            

Alrighty! Here you have it. Your shared expenses application works. It is not completely optimized, but we will definitely work on that as we go ahead. By now you have learnt how to use loops, statements, functions and classes in Python. You should now be comfortable writing and executing your own programs. There is however still a lot to learn, but go on: give it a try. Try writing a basic program that can help make your life a little easier.

In the next chapter, we are going to return to functions and look at some special cases for functions. We'll use these to improve the programs we've worked on and also dive deeper into the world of object-oriented programming.

Seeya!