INTRODUCTION TO PROGRAMMING WITH PYTHON (2015)

CHAPTER 10. CLASSES, OBJECTS, INHERITANCE

Programmers are supposed to be lazy. They have to find the most efficient way of doing things, saving time for more coding or to do what they will.

The purpose of programming is to simply and automate actions and hence avoid repeating writing the same code again. Recall using functions. Once you have created a function, you can call it and re-use it anywhere in the code and the interpreter will know what you are asking for.

However, functions have limitations. They use data but cannot store and hold information. Every time they are called, they start afresh. Now at times certain functions and variables are related to one another very closely, and whenever the function is run, those values need to be fetched and computed alongside new data.

But what if we need a function to generate multiple outputs and values instead of just one output? What you need here is the ability to group multiple functions and associated variables in one place so that they can easily interact with one another.

Take the example of your team.

All of them have joined a golf course and bought new golf clubs.

Your program will store different variables regarding the clubs (their shaft length, weight, forgiveness, spin, etc.), and you want to keep track of the impact each property experiences over time (weakened shaft, increased frustration, scratches, etc.). Suppose you make functions for each team member, what if they decide to have more than one club?

Will you write a whole chunk of code for each different golf club?

Given that most of the clubs share common features, the ideal thing to do would be to create a basic category, or an ideal category that defines all the attributes of the golf club. Hence, whenever you create a new club, all you have to do is specify the new or changed attributes and a new item will be created in the database.

This is where classes and objects come into play. They allow you to create small independent ‘communities’ where functions and variables can interact together, can easily be modified as needed, and remain unchanged (and unaffected by other code).

We start building such objects by first creating classes.

CREATING A CLASS

A class is a blueprint, an idea of something. It does not exist as a usable function, rather it describes how to make something. This blueprint can be used to create a lot of objects.

You can create a class using a class operator. The general syntax for a class is:

class name_of_my_class:

    [statement 1]

    [statement 2]

    [etc]

Here’s an example

class new_shape:

    def __init__(self, x, y):

        self.x = x

        self.y = y

    shape_details = "This is an undescribed shape"

    creator = "No one lays claim to creating this shape "

    def creatorName(self,text):

        self.creator = text

    def detail(self,text):

        self.shape_details = text

def perimeter(self):

        return 2 * self.x + 2 * self.y

def area(self):

        return self.x * self.y

    def scaleSize(self,scale):

        self.x = self.x * scale

    self.y = self.y * scale

The above is a vague description of a shape (square or rectangle).

The description is defined using the variables you have used for the shape , and using the functions you have defined the operations the new class ‘new_shape” can be use for. But you have not created any actual shape. It is simply a description that needs values to define how it would look (the height ‘x’ and width ‘y’), and which will define its properties (area and perimeter).

Wondering what ‘self’ is all about? Recall that you have not created an actual shape yet. ‘self’ is how things are referred to in the class from within itself. It is a parameter that does not pass any value to the function. As a result, the interpreter does not run the code when a class is defined. It considered as a practice in making new functions and passing variables.

All the functions/variables that are created on the first indentation level (the line of code indented right after) are automatically put into self. If a parameter or a function inside the class has to be used within that class, then the name of the function must be proceeded with a self-dot (self.) e.g. self.x as used in the previous code.

USING A CLASS

We can create a class. So, now the question is: how do you use the magical blueprint to create an actual shape?

We use the function _init_ to create an instance of new_shape.

Assuming that the previous code has been run, lets create a shape:

rectangle = new_shape(80,40)

This is where __init__ function comes into play. We have already created a class (a blueprint normally called an instance for the class). We did this by giving it:

·               A name (new_shape)

·               Values in brackets, which are passed pass to the __init__ function.

Using the parameters given to the init functions, the init function generates an instance of that class, assigning it the name rectangle

Now, this new instance of our new_shape class has become a self-contained collection of functions and variables (we will discuss inheritance later). Earlier we were using self. to access the variables and functions defined in that class because we were accessing it from within itself. Now, we will use the name we have assigned to its tangible form (rectangle). We will access the variables and functions in the class from outside.

Once the above code has run, we can access the attributes of our class to create a shape with the following code:

#Calculating the perimeter of the rectangle:

print rectangle.perimeter()

#Calculating the area of the rectangle:

print rectangle.area()

#details about the rectangle

rectangle.describe("A rectangle that is twice as long as it is wide. ")

#Scaling the rectangle to half its size, i.e. making it 50% smaller

rectangle.scaleSize(0.5)

Notice how the assigned name of the new object is being used when the class is being used outside of itself. Whereas, when it was being used from within itself theself. operator was being used.

When used from outside of itself, we can easily change the value of the variables inside the class and access its functions. Think of the class like a factory that assembles new products when given the right input. When using classes, we are not limited to a single instance. We can have multiple of them. For instance, could create another object named thin, long, and sturdy rectangles:

thinrectangle = new_shape(100,5)

longrectangel =new_shape (100,20)

sturdyrectangle = new_shape(100,90)

Even when we have created three instances, all of which relied on the variables and functions from the same class, ALL OF THEM are completely independent of one another and can be used countless times throughout the program.

APPRECIATING THE GEEK TALK

The Object-oriented-programming is a framework with its specific set of words used to describe a programming action. You should know the basic lingo to avoid any confusion in company of another programmer. Here are some basic words:

·               Describing a class means defining it (similar to functions, albeit more detailed)

·               Encapsulation is the grouping of similar functions and variables under a single class

·               Class itself can be used in two instances, to describe the chunk of code that defines a class and the instance where the class is used to create a new object.

·               A class is also known as a ‘data structure’. It can hold data and has the methods to process data provided to it

·               The attribute of a class are the variables inside them

·               A method is the function you have defined inside the class

·               A class itself is an object, and in the same category of things such as dictionaries, variables, lists, etc.

INHERITANCE

We’ve talked about inheritance in functions and earlier in the same chapter. What is it and why is it important? Let’s first recap what we have done so far in terms of functions, variables, and creating classes.

Earlier you saw how we can group a diverse range of variable and functions together. This allows the data and the processing code to be present in the same spot, making it easier to read the code as well to execute it across the code from a single spot. Now, the attributes we granted to the class (variables) and the methods it can use to process the data (the functions), allows us to create innumerable instances of that class without writing new code for every new object that we create.

What if we want to cater an anomaly in our database? Imagine if we have a shape with additional features, ones that does share common features with the original class but has additional features that the original class cannot process?

Creating a new code to cater differences is not the right way to handle it. This is where we make a child class that inherits all the properties of the ideal class while adding its own new features to it.

This is where inheritance comes into play, and which Python makes exceptionally easy to implement.

How does it work?

We define a new class, using the existing class as its ‘parent’ class. Consequently, the child class takes everything from the parent class while allowing us to add new features and attributes and methods to it.

Let’s build on the new_shape class:

Here’s an example

class new_shape:

    def __init__(self, x, y):

        self.x = x

        self.y = y

    shape_details = "This is an undescribed shape"

    creator = "No one lays claim to creating this shape "

    def creatorName(self,text):

        self.creator = text

    def detail(self,text):

        self.shape_details = text

def perimeter(self):

        return 2 * self.x + 2 * self.y

def area(self):

        return self.x * self.y

    def scaleSize(self,scale):

        self.x = self.x * scale

    self.y = self.y * scale

Now square is also considered a form of the rectangle, however its width and length are equal to one another. Now if we want to define a new class using the existing new_shape class, the code would look like this:

class square(new_shape):

    def __init__(self,x):

        self.x = x

              self.y = x

This is quite similar to how we defined the new_shape class, except that we have used the parent class as the set of parameters that need to be inherited. Notice how easy it was to cater this new object without having to redefine and write its code separately.

We changed only what needed to be changed, borrowing the rest. We have merely redefined __init__ function of new_shape so that the height and width (x and y) become the same. However, the new variables defined in the child class will never overwrite the ones present in new_shape.

Now, square itself has become a new class and we can use it to create another class as well! You see a pattern here? Its like a lego puzzle, you create objects and you use them as bricks to build better and more complex objects and programs.

Let’s create a double square, so that two squares are created side by side:

class 2square(Square):

    def __init__(self,y):

        self.y = y

        self.x = 2 * y

    def perimeter(self):

        return 2 * self.x + 3 * self.y

Notice that this time we have an additional method (a function) in the code as well. We have redefined the perimeter function because the one that square has inherited form new-shape cannot cater to the needs of this new class.

If you create an instance of 2square, your double squares will all have the same attributes and properties defined by the 2square class and not the square or new_shape.

POINTERS AND DICTIONARIES OF CLASSES

Previously, we had seen how variable swapping works, e.g. var1=var2 would swap the left hand side variable with the value stored in the right hand side variable.

The same does not hold true when it comes to creating class instances. When you write instance1=instance2, what is actually happening is that the first class is pointing to the class on the right. “Pointing” means that both the names of instances refer to the same class instance, and that the same class instance can be used by either name.

This brings us to dictionaries of classes.

Building on how pointers work, Python lets us easily assign instance of class to an entry in a list or dictionary as well. But why do it? What’s the benefit?

It allows us to create virtually unlimited number of class instances to run from our program. Here’s an example of using pointers to create dictionary instances. Assuming the original definition of new_shape, and that the square and 2sqaure classes have been run:

#Create a dictionary:

dictionary = {}

#Next, create some instances of classes in the dictionary:

dictionary["2square 1"] = 2square(5)

dictionary["long rectangle"] = new_shape(100,30)

Now you can use them like normal classes!

dictionary["2square 1"].creatorName("Python Coder")

print dictionary["2square 1"].author

print dictionary["long rectangle"].area()

Now, you have replaced the previous name we had created for our creation with an arguably better name by creating a new dictionary entry.