In [1]:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import pandas as pd

Lecture 10:

  • Learn about "object oriented programming" (OOP)

  • Learn how to create a "class"

  • Learn more about namespaces

  • Learn more about copies

Object oriented programming

Until now we haven't mentioned object oriented programming (OOP), yet we have been using objects from the beginning. Knowing how to create and use objects in Python is very powerful. Examples of objects that we have already encountered are the various data structures we have been using and things like plots. Objects have methods that can be used to change an object and attributes that describe features of an object.

Now we will learn how to make our own objects with our own special blend of attributes and methods. The trick is to make a class and define it to have the desired attributes and methods.

Classes

To create an object with methods, we use a class definition, which is a blueprint or recipe defining the attributes and methods associated with the class. When we call the class, we create an instance of the class, also known as an object.

Here is an example of a class definition:

In [2]:
class Circle:
    """
    This is an example of a class called Circle
    """
    import numpy as np # get some math power
    # define some attributes of the Circle class
    pi=np.pi # pi is now an attribute of this class too.  
    # initialize the class   with the attribute r (no parentheses when called)
    def __init__(self,r):
        self.r=r # define a variable, r
    # define some methods (these have parentheses when called)
    def area(self): 
        return (1./2.)*self.pi*self.r**2
    def circumference(self):
        return 2.*self.pi*self.r

Now we can create an instance of the Circle class called C with a radius of r.

In [3]:
r=3.0 # assign 3 to a variable r
C=Circle(r) # create a class instance with radius of 3.0

We can use any of the attributes or methods of this class like this:

In [4]:
print ("The value of pi is: ",C.pi) # no parentheses!
print ("The radius of this circle is: ",C.r)# no parentheses!
print ("The area of a circle with radius = ",r,'is: ',C.area()) # with parentheses!
print ("The circumference of that circle is: ",C.circumference()) # with parentheses!
The value of pi is:  3.141592653589793
The radius of this circle is:  3.0
The area of a circle with radius =  3.0 is:  14.137166941154069
The circumference of that circle is:  18.84955592153876

We can also save the Circle class in a module, just as we did in earlier Lectures for functions. Then we can import it into other notebooks of scripts as desired.

In [5]:
%%writefile Shapes.py
class Circle:
    """
    This is an example of a class called Circle
    """
    import numpy as np # get some math power
    # define some attributes of the Circle class
    pi=np.pi # pi is now an attribute of this class too.  
    # initialize the class   with the attribute r (no parentheses when called)
    def __init__(self,r):
        self.r=r # define a variable, r
    # define some methods (these have parentheses when called)
    def area(self): 
        return (1./2.)*self.pi*self.r**2
    def circumference(self):
        return 2.*self.pi*self.r
Writing Shapes.py

Now we can use it! Here is an example how:

In [6]:
import Shapes as S
newCirc=S.Circle(6.0)
print (newCirc.pi)
3.141592653589793

Attributes and methods

You might be wondering about some things by now. For example, you should have noticed is that when we asked for C.pi there were no parentheses, but both C.area( ) and C.circumference( ) did have parentheses. Why?

The answer is that r and pi are attributes, and area and circumference are methods. Did you notice that the method definitions look a lot like functions, but are inside the class definition. A method really is a function, but it is special in that it belongs to a class and works on the instance of the class. They can only be called by using the name of the instance, followed by a dot, followed by the method (with parentheses).

More about classes

Classes are not the same as functions. Although our Shape module can be imported just the same as any other module, to use it, we first have to create a class instance (C=Shapes.Circle(r)).

All methods (parts that start with def), have an argument list. The first argument has to be a reference to the class instance itself, which is always self, followed by any variables you want to pass into the method.

The "init" method initializes the instance attributes. In the Circle class, the __init__ method defined the attribute r, which gets passed in when the class is first called.
Asking for any attribute, retrieves the current value of that attribute.

But. Attributes can be changed:

In [7]:
print (C.r)
C.r=7.
print (C.r)
3.0
7.0

The methods (area and circumference) are defined just like any function except note the use of self as the first argument. This is required in all class method definitions. In our case, no other variables are passed in because the only one used is r, so the argument list consists of only self. Calling these methods requires no further arguments (the parentheses are empty) and the class returns the current values.

In [8]:
C.area()
Out[8]:
76.96902001294993

You can make a subclass (child) of the parent class which has all the attributes and methods of the parent, but may have a few attributes and methods of its own. You do this by setting up another class definition within a class.

So, the bottom line about classes is that they are in the same category of things as variables, lists, dictionaries, etc. That is, they are "data structures" but with benefits. They hold data, and they also hold the methods to process those data.

If you are curious about classes, there's lots more to know about them that we don't have time to get into. You can find useful tutorials online: https://www.python-course.eu/python3_object_oriented_programming.php

or

http://www.sthurlow.com/python/lesson08/ [but be careful with this one as it is for Python 2.7, so the print statements won't work without parentheses, e.g., print ('this way'), not, print 'not this way'. ]

Namespaces

Another thing you might be wondering about is why did we import NumPy inside the class definition when it was imported into the notebook at the top? The answer is we didn't have to. The class definition works perfectly well without it in this case. But if we don't import Numpy within in the Shape module, the module won't work at all because it doesn't "know" about NumPy. So in the module, you have to import whatever you need to run the module.

Copies

Another issue we have been tiptoeing around is the concept of a copy of an object and what that means. In Python, this can be a bit confusing. When we define some simple variables, the behavior is pretty much what you might expect:

In [10]:
x=3 # define x
y=x # set y equal to x
print (y) # print out y
x=4 # change the value of X
print (y) # and y is still equal to its first definition.  
3
3

But if we define a list object (a compound object with more than one variable), things get weird:

In [11]:
L1=['spam','ocelot',42] # define the list
L2=L1  # make a copy of the list
print (L2) # print the copy
L1[2]='not an ocelot' # change the original
print (L2) # and oops - the copy got changed too!
['spam', 'ocelot', 42]
['spam', 'ocelot', 'not an ocelot']

This means that L1 and L2 refer to the SAME OBJECT. So how do I make a copy that is its own object (doesn't change)? For simple lists (that do not contain sublists), we already learned how to do this:

In [12]:
L3=L1[:]
print (L3)
L1[2]=42
print (L3)
['spam', 'ocelot', 'not an ocelot']
['spam', 'ocelot', 'not an ocelot']

This trick breaks down if the object is more complicated. The copies will sometimes be subject to mutation. (Try this yourself!).

To avoid this problem, there is a module called copy with a function called deepcopy, which will make an independent copy of the object in question:

In [13]:
from copy import deepcopy

L1=['spam','ocelot',42] # define the list
L2=deepcopy(L1)  # make a copy of the list
print ("L2: ",L2) # print the copy
L1[2]='not an ocelot' # change the original
print ("L1: ",L1)
print ("L2: ",L2) # and bingo, L2 didn't 
L2:  ['spam', 'ocelot', 42]
L1:  ['spam', 'ocelot', 'not an ocelot']
L2:  ['spam', 'ocelot', 42]
In [14]:
# clean up
!rm Shapes.py
In [ ]: