DEV Community

Cover image for Part Eight: Classes and Objects
Simon Chalder
Simon Chalder

Posted on • Updated on

Part Eight: Classes and Objects

"I used to be enamoured of object-oriented programming. I'm now finding myself leaning toward believing that it is a plot designed to destroy joy." — Eric Allman


Welcome to part eight. In this series I hope to introduce the basics of coding in Python for absolute beginners in an easy to follow and hopefully fun way. In this article we will look at the basics of using classes and objects to make modular code which is easier to work with.


Classes, Objects, and this thing called 'Object Orientated Programming'


If you have been reading around the subject matter, or maybe just browsing coding sites online, you may have come across the term 'object orientated programming' being thrown around. So what is it and why should you care?

Up to now we have been writing what might be termed 'procedural code' where the interpreter starts at the top and works down line by line in a single Python file. Now don't get me wrong, you can accomplish a lot with this style of coding, and many times this may be all we need to accomplish our goals. However, the downside of this kind of coding rears its head when our applications start to get bigger and more complex. Simple applications or 'scripts' as they are often referred to, tend to set out to accomplish a single task. But what if we want our application to do several things? Using procedural coding we would end up with 'monolithic code' which means just a single, giant file with hundreds or even thousands of lines of code. When our code files get too big they become difficult to keep track of and when we have problems it makes it much harder to determine where the problem is arising from.

Into the breach steps object orientated programming (from here on out I will refer to it as 'OOP' as it takes a long time to type!). In OOP, as the name suggests, we use objects in our code. We will come onto what objects are and how to make them shortly. However, using objects and their associated classes (more on this in a little while), we can break our code down into more manageable chunks and make it all more modular.

Classes and objects allow us to group together related variables and functions into specialist tools which we can use to accomplish a task.

Let's say we decide to write an application which does the following things:

  1. Connect to the internet and navigate to a web page or database which hosts wildlife datasets
  2. Download the required dataset
  3. Open or read the data file
  4. Perform some calculations with the data
  5. Display the result in the form of a graph

Writing this in a single file with all of the associated variables and functions required might make for a pretty large file which is then difficult to work with. However, we could use classes and objects to break this application down into its component parts to make it easier to work with. We could have a class that deals with connecting to the internet, another which deals with downloading a file, and so on. This means if there is an issue in the code we know exactly where to look for the problem.


Classes and Objects


There is no discussion about OOP unless we talk about the objects and classes themselves. What are they? What do they do? OOP requires some different thinking and can be hard to understand at first. Try to understand on a conceptual level what is happening first and worry about the syntax after.

OK here goes.

Imagine we want to create a paper form so that we can record details about a company vehicle. We want a standardised form so we pay a printing company to make up a pad full of the uncompleted forms we have designed. In coding terms, a class is like the pad of printed forms, it is a template from which we will make all of our objects. Every form has fields such as the name of the person completing the form, the date, vehicle registration, mileage etc. When we fill out all of the fields on the form and tear it off the pad we now have an object - a completed version of the paper form which contains unique information about that vehicle.

Here is an example of the form (class):

my_vehicle_form:
    make = 
    model = 
    mileage = 
    doors = 
    colour = 
Enter fullscreen mode Exit fullscreen mode

When we fill it in, we now have a a completed version of the form with all of its associated fields but this one also has values for the fields making it a unique copy based on the form template which looks like this:

land_rover:
    make = land rover
    model = defender
    mileage = 88625
    doors = 5
    colour = green
Enter fullscreen mode Exit fullscreen mode

By doing this we have taken the form template (the class) and made an object from it which contains the details above. We could also make another object from the same class template:

pickup:
    make = Ford
    model = ranger
    mileage = 12688
    doors = 3
    colour = blue
Enter fullscreen mode Exit fullscreen mode

Now we have 2 objects created from the same class. The class always remains the way we have created it, it is the template all objects of that class will be cast from just like a mould or rubber stamp. We can go on to make any number of objects from a class. We can also have as many classes as we like for lots of different purposes.


Creating a Class


Creating a class in Python is not that different from the example above. We simply use the keyword class followed by the name of the class we want to make and then a ':'.

class my_first_class:
Enter fullscreen mode Exit fullscreen mode

Next, we move to the next line down and indent to write code inside the class:

class my_first_class:
    # write code here
Enter fullscreen mode Exit fullscreen mode

So what goes inside our classes? Classes can have their own variables known predictably as 'class variables' or 'attributes', as well as their own functions known as 'methods'.

Let's look at creating some attributes first. We will use our vehicle example from above. First, we create our class and then we add attributes in our indented code in the same way as we create a normal variable. Note that here we are giving the class predetermined attributes that will apply to all objects made from it, in other words all objects made from our vehicle class will be blue and have 5 doors. We will cover giving new objects their own attributes later on.

class vehicle:
    colour = "blue"
    doors = 5
Enter fullscreen mode Exit fullscreen mode

Our class template is now ready to be used to create an object, so lets do that.

To create an object from a class we name our object and then assign it to the class using [object name] = [class name]:

my_truck = vehicle
Enter fullscreen mode Exit fullscreen mode

We have now created a new object named my_truck from the vehicle class.


Let's talk about inheritance


Here is the whistle-stop explanation of inheritance, a subject which could be the topic of a series of articles itself.

By 'inheritance' I'm not referring to that rich uncle you're hoping will leave his fortune to you. In coding, inheritance refers to the way that objects 'inherit' attributes and methods from their class template.

Whichever attributes and methods are written in the class are passed down to an object when it is created. If those attributes have values assigned to them, the values are also passed down. Keep this concept in mind as we move forward.


We have now created our object which we named my_truck from the vehicle class. Our object inherits all of the attributes its template class had so our object also has 'colour' and 'doors' attributes. Because our class had values assigned to those attributes, our newly created object will also have its colour attribute set to a value of 'blue' and a doors attribute of '5'.

To access an attribute, we use the name of the object followed by a '.' and then the name of the attribute:

print(my_truck.colour)

# Output
'blue'

print(my_truck.doors)

# Output
5
Enter fullscreen mode Exit fullscreen mode

Note that if our class attributes have values we can also access them directly:

print(vehicle.colour)

# Output
'blue'
Enter fullscreen mode Exit fullscreen mode

We can create classes with pre-assigned values to act as a kind of reference but most of the time this is not what we want (more on this later). In the above example, every object we create from the 'vehicle' class will be the same - it will have a colour set to 'blue' and doors set to '5'. What if our next vehicle is red and has 3 doors?

What we need is a way to pass data to an object as we are creating it so the object is unique and not a simple clone of the class template.


'self' and __init__()


No, those aren't typos in the title. They are concepts we need to understand in order to work with classes and objects effectively. At this point I'm not going to lie, this may require reading through a few times and probably using some other sources to understand. So buckle up kids, here we go!

Let's tackle __init__() first. When we want to create an object that is unique, we need a way to give it unique values for its attributes at the time of its creation. To do this we use what is known as a 'constructor'. In Python __init__() is the constructor and is simply a function which runs when an object is created. Don't be put off by those weird '__' lines either side of the name, this is just how Python recognises what it does. Just like the functions we have already come across, it accepts arguments. Those arguments are then used to give values to the object's attributes.

With me so far?

OK, this brings us to 'self'. Consider the example below:

my_name = "Harry"

class my_class:
   my_name = "Helen"

my_object = my_class
Enter fullscreen mode Exit fullscreen mode

Here we have an object my_object which is created from the my_class class and as such inherits its attributes. We also have an unrelated variable my_name. This means in this code we have 3 variables named my_name. One is the standard variable, another is in the class attribute, and another in the object's attribute. As you can imagine this gets confusing and when we try to access my_name, Python is not sure which of the 3 possible versions we are referring to. To solve this we use 'self' to indicate we are referring to the attribute in the object and not the class or some other variable with the same name.

Let's put this all together to try and demonstrate this a little better. First let's create our class using a constructor like we discussed above. We can also name a couple of attributes. Note that we can leave the attribute values blank to create empty attributes we can then fill with a constructor.

class my_class:
   def __init__():
       name = 
       age = 
Enter fullscreen mode Exit fullscreen mode

We now need to give our constructor some arguments which will be used to assign values to the attributes when our object is created. If we simply put 2 arguments in the brackets of __init__(), Python will not know if we wish to change the value of the object's attribute or some other with the same name:

class my_class:
   def __init__(name, age):
       name = name
       age = age
Enter fullscreen mode Exit fullscreen mode

Therefore we must use 'self' to indicate we are dealing with the created object and not another part of the code. For this purpose, 'self' must always be the first argument we give to our constructor, even if no other arguments are given. Let's now add 'self.' before the attribute names. In this manner, 'self' is basically a place holder for the name of the object, whatever that might be when we create it:

class my_class:
   def __init__(self, name, age):
       self.name = name
       self.age = age
Enter fullscreen mode Exit fullscreen mode

When we come to create our object we now pass our arguments but we ignore 'self', we do not need to pass a value for that:

my_object = my_class("Jack", 34)
Enter fullscreen mode Exit fullscreen mode

Our object my_object has now been created from the my_class template and it has been assigned the name attribute equal to 'Jack' and an age attribute equal to '34'.

At this point, you may have a headache and be questioning why you decided to try this coding nonsense to begin with. It can be difficult to understand at first, but the best way to gain an understanding is to practice examples of creating classes and objects, so let's do that now.


A Practice Example


Here is an example to help reinforce the OOP principles we have covered so far.

Let's create a new class called 'mammal'. Inside the class, we define our constructor and give it the argument 'self':

class mammal:
    def __init__(self):
Enter fullscreen mode Exit fullscreen mode

Now let's choose some attributes for our mammals:

class mammal:
    def __init__(self, colour, diet, habitat, offspring):
        self.colour = colour
        self.diet = diet
        self.habitat = habitat
        self.offspring = offspring
Enter fullscreen mode Exit fullscreen mode

Now we have our class set up let's make some objects from the class. Each argument is used to assign a value to the relevant attribute. When we create the fox object, the first argument we pass - 'red-brown' is assigned to fox.colour, 'carnivore' is assigned to fox.diet and so on. Remember in the class template, 'self' is just a placeholder for the names of the objects we will create:

fox = mammal('red-brown', 'carnivore', 'farmland', 2)

rabbit = mammal('grey-brown', 'herbivore', grassland', 10)
Enter fullscreen mode Exit fullscreen mode

We now have 2 unique objects created from the mammal class which each have their own values for their attributes. Let's test it to make sure:

print(fox.colour)

# Output
'red-brown'

print(rabbit.diet)
'herbivore'
Enter fullscreen mode Exit fullscreen mode

For the visual learners amongst us, here is a visual example to try to show things more clearly.

mammal

The whole 'self' thing can take a bit of getting used to but hopefully you are beginning to see how we create classes and then objects from those classes.


Methods


A method is simply a function which belongs to a class. Class methods are inherited by any object that is made from them just like attributes. To create a method we write it just like a normal function except indented in the class. Here is an example:

class say_something:
    def speak_word(word):
        print(word + "!")
Enter fullscreen mode Exit fullscreen mode

To call a class method we use the following syntax - [class/object].[method]()

So to call our method from the example above we would use:

speak = say_something

speak.speak_word("Hello")

# Output
'Hello!'
Enter fullscreen mode Exit fullscreen mode

Methods, just like normal functions, accept arguments. In the example above we pass the argument 'word' when calling the method.


Class Methods vs Object Methods


Methods can be called both from the original class and from objects made from that class. For example we can create the following class and object from the class:

class greeting:
    def __init__(self, greeting):
        self.greeting = greeting

    def say_greeting(self):
        print(self.greeting)

my_object = greeting("Hello!")
Enter fullscreen mode Exit fullscreen mode

We can now call 2 versions of the say_greeting() method - 1 from the class and 1 from the method. Remember class methods can be called directly but are also inherited by all objects made from that class:

greeting.say_greeting()

# Output
'' # the greeting class has no pre-coded value for the 'greeting' attribute

my_object.say_greeting()

# Output
'Hello!'
Enter fullscreen mode Exit fullscreen mode

So why is this important? We can use classes in different ways. We can use them in the normal fashion as we have seen above, as templates from which to create objects. However, we can also use 'static classes' to create a kind of reference or toolkit.

Static classes are not used to create objects and as such do not pass on inherited attributes or methods.

So what's the point of them?

Well, remember the calculator application we made in the function article? We could make a class which holds our calculation methods and then simply call them when required.

class calculator:
    def add(num1, num2):
        return num1 + num2

    def sub(num1, num2):
        return num1 - num2

    def div(num1, num2):
        return num1 / num2

    def mul(num1, num2):
        return num1 * num2
Enter fullscreen mode Exit fullscreen mode

Effectively what we have done here is to make our own built in function like print(), or float(), which we can use any time we need it. We do not need to create an object from the calculator class in order to access its methods. However, we must ensure we use the name of the class when calling it e.g. calculator.add().

result = calculator.add(23, 5)
print(result)

# Output
28
Enter fullscreen mode Exit fullscreen mode

In addition, we can save this class in its own Python file to be imported and used in any future projects which might require it. I will cover how to do this in a future article.

As you have probably figured out, there is a lot to learn with classes and objects. The good news is if you can understand the basics, you can do an incredible amount with that level of knowledge. Remember you don't need to know everything and you don't need to be at the standard of a professional developer to write great applications.


Challenge


I know your head is probably hurting at this point but the best way to understand this stuff is through practice. I'm going to set a challenge to test your use of classes and objects. Hopefully nothing too intense. Take your time and think it through. Unlike other challenges, I will place one possible solution after the challenge in this article so that if you get completely stuck you can go through it step by step. However, I strongly encourage you to try and work it out yourself first.

Let's write an application to put some of this into practice. We will write code that will take input about the details of a piece of land, perform some analysis and then output some (hopefully) useful data.

Here is what the code must do:

  1. Create a class for a piece of farmland called 'farmland'
  2. Inside your new class create some attributes with short but appropriate names for:
    • Total area
    • Unharvested cereal headland
    • Winter bird food
    • Flower rich field margins
    • Winter stubble
  3. Define 4 class methods to calculate each payment type. Each method should accept the size in hectares of that land type and the current payment rate from the Countryside Stewardship scheme. Current rates can be found here.
  4. Create an object from your class and pass as arguments the total area of the land (hectares) as well as the size of any areas of cereal headland, winter bird food etc. You can simply enter these values when you create the object or get them through user input, your choice.
  5. Calculate the relevant totals and print out the results for that piece of land.
  6. BONUS - make the application run on a loop so that multiple pieces of land can be analysed until the user decides they have done enough.
  7. BONUS - make another class based on another land type such as wetland, woodland etc and incorporate this into your application.

Solution


Here is one possible solution to the challenge above. Remember, there are usually several different ways of tackling problems in programming. If your answer is different, but it works, then well done!

We begin by creating the farmland class, defining the constructor, and setting attributes. It is certainly valid to create an argument for each payment rate, but I have combined them into a single argument which will be a list. The benefit of this is if I want to change the rates in the future they are all together rather than spread out amongst the code:

class farmland:
        def __init__(self, total_area, cereal, birds, margins, stubble, rates):
            self.total_area = total_area
            self.cereal = cereal
            self.birds = birds
            self.margins = margins
            self.stubble = stubble
            self.rates = rates
Enter fullscreen mode Exit fullscreen mode

Next we define class methods for each payment type. Each method calculates the total of the relevant payment rate multiplied by the area of that land type. Remember I have used a list for the rates, hence rates[0], rates[1] etc.

def cereal_headland_calc(self):
            return self.rates[0] * self.cereal

        def winter_bird_food_calc(self):
            return self.rates[1] * self.birds

        def flower_rich_margins_calc(self):
            return self.rates[2] * self.margins

        def winter_stubble_calc(self):
            return self.rates[3] * self.stubble
Enter fullscreen mode Exit fullscreen mode

Also remember to include 'self' as an argument here as we will be calling this method from the object and not the class. Next, we create another method to display the results. Each of the above calculation methods is called and the result stored in a variable which is then printed to the screen as a string:

def show_results(self):
            cereal_calc = self.cereal_headland_calc()
            bird_calc = self.winter_bird_food_calc()
            margins_calc = self.flower_rich_margins_calc()
            stubble_calc = self.winter_stubble_calc()

            print("Results for Farmland Analysis:\n")
            print("Total Area: " + str(self.total_area))
            print("Potential Unharvested Cereal Headland Payment    - £" + str(cereal_calc))
            print("Potential Winter Bird Food Payment               - £" + str(bird_calc))
            print("Potential Flower Rich Field Margin Payment       - £" + str(margins_calc))
            print("Potential Winter Stubble Payment                 - £" + str(stubble_calc))
Enter fullscreen mode Exit fullscreen mode

We are now done with our class. Outside of our class, we can now create a function to get data from the user:

def get_input():
        area_input = float(input("Enter Total Area (ha): "))
        cereals_input = float(input("Enter Unharvested Cereal Headland Area (ha): "))
        birds_input = float(input("Enter Winter Bird Food Area (ha): "))
        margins_input = float(input("Enter Flower Rich Field Margin Area (ha): "))
        stubble_input = float(input("Enter Overwinter Stubble Are (ha): "))
        return area_input, cereals_input, birds_input, margins_input, stubble_input
Enter fullscreen mode Exit fullscreen mode

Before we call our get_input() function, we need to set the rates for the potential payments:

cs_farm_rates = [640, 640, 628, 493]
Enter fullscreen mode Exit fullscreen mode

To get our user input, we call get_input() and assign the returned values to some variables:

area_input, cereals_input, birds_input, margins_input, stubble_input = get_input()
Enter fullscreen mode Exit fullscreen mode

If we now create an object from our class we can call the show_results() method from that object:

new_land = farmland(area_input, cereals_input, birds_input, margins_input, stubble_input, cs_farm_rates)

new_land.show_results()
Enter fullscreen mode Exit fullscreen mode

Up to now the full application looks like this. Make a note of the indentation to ensure the correct code is included or excluded from our class and function:

class farmland:
        def __init__(self, total_area, cereal, birds, margins, stubble, rates):
            self.total_area = total_area
            self.cereal = cereal
            self.birds = birds
            self.margins = margins
            self.stubble = stubble
            self.rates = rates

        def cereal_headland_calc(self):
            return self.rates[0] * self.cereal

        def winter_bird_food_calc(self):
            return self.rates[1] * self.birds

        def flower_rich_margins_calc(self):
            return self.rates[2] * self.margins

        def winter_stubble_calc(self):
            return self.rates[3] * self.stubble

        def show_results(self):
            cereal_calc = self.cereal_headland_calc()
            bird_calc = self.winter_bird_food_calc()
            margins_calc = self.flower_rich_margins_calc()
            stubble_calc = self.winter_stubble_calc()

            print("Results for Farmland Analysis:\n")
            print("Total Area: " + str(self.total_area))
            print("Potential Unharvested Cereal Headland Payment    - £" + str(cereal_calc))
            print("Potential Winter Bird Food Payment               - £" + str(bird_calc))
            print("Potential Flower Rich Field Margin Payment       - £" + str(margins_calc))
            print("Potential Winter Stubble Payment                 - £" + str(stubble_calc))

    def get_input():
        area_input = float(input("Enter Total Area (ha): "))
        cereals_input = float(input("Enter Unharvested Cereal Headland Area (ha): "))
        birds_input = float(input("Enter Winter Bird Food Area (ha): "))
        margins_input = float(input("Enter Flower Rich Field Margin Area (ha): "))
        stubble_input = float(input("Enter Overwinter Stubble Are (ha): "))
        return area_input, cereals_input, birds_input, margins_input, stubble_input

    cs_farm_rates = [640, 640, 628, 493]

    area_input, cereals_input, birds_input, margins_input, stubble_input = get_input()

    new_land = farmland(area_input, cereals_input, birds_input, margins_input, stubble_input, cs_farm_rates)

    new_land.show_results()
Enter fullscreen mode Exit fullscreen mode

If we now run this code and enter some values we are presented with output similar to the image below:

Output
That last result shows we need to round the total to 2 decimal places. We can do this in the following way by altering our code with the round() built in function. The number after the comma represents the number of decimal places:

def cereal_headland_calc(self):
        return round(self.rates[0] * self.cereal, 2)

    def winter_bird_food_calc(self):
        return round(self.rates[1] * self.birds, 2)

    def flower_rich_margins_calc(self):
        return round(self.rates[2] * self.margins, 2)

    def winter_stubble_calc(self):
        return round(self.rates[3] * self.stubble, 2)
Enter fullscreen mode Exit fullscreen mode

The final part of the challenge is to make the app loop until the user decides to quit. To do this, we simply indent the entire code inside of a while loop with some flow control at the end. The finished application should now look like this:

run = True
while run == True:    
    class farmland:
        def __init__(self, total_area, cereal, birds, margins, stubble, rates):
            self.total_area = total_area
            self.cereal = cereal
            self.birds = birds
            self.margins = margins
            self.stubble = stubble
            self.rates = rates

        def cereal_headland_calc(self):
            return round(self.rates[0] * self.cereal, 2)

        def winter_bird_food_calc(self):
            return round(self.rates[1] * self.birds, 2)

        def flower_rich_margins_calc(self):
            return round(self.rates[2] * self.margins, 2)

        def winter_stubble_calc(self):
            return round(self.rates[3] * self.stubble, 2)

        def show_results(self):
            cereal_calc = self.cereal_headland_calc()
            bird_calc = self.winter_bird_food_calc()
            margins_calc = self.flower_rich_margins_calc()
            stubble_calc = self.winter_stubble_calc()

            print("Results for Farmland Analysis:\n")
            print("Total Area: " + str(self.total_area))
            print("Potential Unharvested Cereal Headland Payment    - £" + str(cereal_calc))
            print("Potential Winter Bird Food Payment               - £" + str(bird_calc))
            print("Potential Flower Rich Field Margin Payment       - £" + str(margins_calc))
            print("Potential Winter Stubble Payment                 - £" + str(stubble_calc))

    def get_input():
        area_input = float(input("Enter Total Area (ha): "))
        cereals_input = float(input("Enter Unharvested Cereal Headland Area (ha): "))
        birds_input = float(input("Enter Winter Bird Food Area (ha): "))
        margins_input = float(input("Enter Flower Rich Field Margin Area (ha): "))
        stubble_input = float(input("Enter Overwinter Stubble Are (ha): "))
        return area_input, cereals_input, birds_input, margins_input, stubble_input

    cs_farm_rates = [640, 640, 628, 493]

    area_input, cereals_input, birds_input, margins_input, stubble_input = get_input()

    new_land = farmland(area_input, cereals_input, birds_input, margins_input, stubble_input, cs_farm_rates)

    new_land.show_results()

    run_again = input("Perform another analysis 'y' or 'n'?")
    if run_again.upper() == 'Y':
        run = True
    else:
        run = False
Enter fullscreen mode Exit fullscreen mode

If you want to add analysis for a different land type, you simply need to create another class with appropriate methods and rates. This is one of the advantages of OOP, we can create modular classes and objects where all of the related code is together in one place.

I hope all of this made sense and you are starting to see things more clearly. If not, please use as many online sources as you can until you find one which explains it best. If you are lucky enough to know someone with experience of OOP you may benefit from them going through it with you. As I have mentioned, these can be tricky concepts to grasp at first and they can also be difficult to express in a way which is easy for the learner. Hang in there though and practice as much as you need. Take your time and consult as many sources and learning materials as necessary.


Conclusion


That was a tough one to get through but we are nearly a the end of the series. In the next article I am going to talk about importing your own (and other people's) files and modules into your projects.

Thank you as always for reading and I look forward to any constructive feedback you may have on this article or the series as a whole. Keep practising and I look forward to seeing you next time.

Simon.

Top comments (0)