Classes and Objects

In several lessons I’ve talked about data types. The type function shows you the type of the contents of a variable. Python enables you to create your own data types and give them any functions you like. In this lesson you’ll learn how to do that. Python is an object oriented programming language. A data type has three essential properties:

  1. A name

  2. Member variables (or data)

  3. Member functions

Data types are defined with the keyword class (like functions are defined with def). Let’s start with a class that has a name and nothing else:

class Simple:
    """My Simple Class"""

Just like functions classes have docstrings to document what they do. Use the next cell to enter the definition of the Simple class.

[ ]:

A class is like a template for making something (like a cookie cutter). You use the template to make an object (called an instance). The distinction between the class and the instance is subtle at first, just try to remember:

  • The class is a cookie cutter

  • The instance is a cookie

Here’s showing how to get an instance of an object:

inst = Simple()

You’ve seen this before! Remember getting a Turtle? It’s just creating an instance of the Turtle class. In the next cell create an instance of Simple and use type to see its type:

inst = Simple()
print("The type of simple is:", type(inst))
[ ]:

You can see how the docstring of Simple works using the help function:

help(Simple)
[ ]:

Each instance is like a world of its own, like a gingerbread cookie you can decorate it however you want and the decorations are different for every cookie. Here’s how you add variables into an instance using the dot . operator.

one = Simple()
one.name = "Mike"
one.job = "Instructor"

Try that example:

[ ]:

The instance of simple called one now contains two variables name and job. Let’s do that to another instance:

two = Simple()
two.name = "Dan"
two.fam = "Mike's Brother"

Now we have a second instance named two. Here’s how you access the variables in an instance:

print(one.name)
print(two.name)

Enter the example code into the next cell:

[ ]:

Each instance can contain as many variables as you like and they’re all different. The variables name, job and fam are called member variables because they live inside of an instance.

When you use an instance as an argument, it’s like passing all of the member variables along with it. Here’s an example of a function that uses an instance of the Simple class:

def print_stuff_in_simple(inst):
    print ('Name is:', inst.name)
    print ('Job is:', inst.job)

print_stuff_in_simple(one)

Enter the example code:

[ ]:

Notice how ``name`` and ``job`` are attached to ``one``. The print_stuff_in_simple function takes one argument but, since that argument is a class instance, there could be hundreds of variables inside that argument. It’s a bit like a dictionary!

Member Functions

It’s common to want functions that do stuff with class instances (like print_stuff_in_simple). Functions can be defined inside of a class definition and those functions become a part of the class, just like the variables. For example, if we want a function that prints the name member variable we can define the class like this:

class Simple:
    """A somewhat less simple class."""

    def print_stuff(self):
        """Print the name variable."""
        print ('Name is:', self.name)

Notice that the function is inside of the class. Enter the example into the next cell:

[ ]:

When you run the cell, nothing happens. Let’s see how to use the print_stuff function:

inst = Simple()
inst.name = "Your Name"
inst.print_stuff()

Try the example in the next cell:

[ ]:

Now, for the first time, you see what the . really does when you put a function to the right. It calls a member function!

Understanding the self Variable

A member function is just a normal function. Since it works on a class instance it must receive a copy of the class instance. Member functions are called in a special way that ensures the instance is always passed as the first argument to the function. The name self is not speical in Python but you see it a lot. Class member functions always have the variable self as their first argument. Though you can use any name the use of self is an important tradition.

The Self Variable

The use of self ensures that the function is always working on the instance you expect. Here’s an example that shows the power and usefulness of the self variable:

one = Simple()
one.name = "Mike"
two = Simple()
two.name = "Dan"
one.print_stuff()
two.print_stuff()

Enter the example into the next cell:

[ ]:

Notice how each ``print_stuff`` call goes to the right instance? It’s because the function receives the instance in self. Member functions can have any number of arguments, just like regular functions, but self always appears first:

class Simple:
    """A somewhat less simple class."""

    def print_stuff(self):
        """Print the name variable."""
        print ('Name is:', self.name)

    def input_name(self, prompt):
        """Ask the user for a name."""
        self.name = input(prompt)

The input_name function takes one argument in addition to self. It is called like this:

inst = Simple()
inst.input_name("Please type your name: ")

Try it in the next cell:

[ ]:

The __init__ Member Function

There are special member functions that have useful properties. Special member functions begin and end with a double underscore __. The most important one to know is the __init__ function. The __init__ function is automatically called when a new instance of a class is created.

class Simple:
    """A somewhat less simple class."""

    def __init__(self):
        """Initialize the class."""
        print("Initializing the class...")
        self.name = "(no name)"

    def print_stuff(self):
        """Print the name variable."""
        print ('Name is:', self.name)

Enter the new version of Simple into the next cell and make an instance:

[ ]:

Can you see the ``__init__`` function work? Use the debugger if not. The __init__ function can take arguments. You have to supply those arguments when you create a new instance of the class. For example, the __init__ function could set the initial name:

class Simple:
    """A somewhat less simple class."""

    def __init__(self, name):
        """Initialize the class."""
        self.name = name

    def print_stuff(self):
        """Print the name variable."""
        print ('Name is:', self.name)

To create a Simple now we need to give an initial name:

one = Simple("Mike")
two = Simple("Dan")
[ ]:

The purpose of the __init__ function is to initialize all of the member variables in the class. For example, if a blog post has a title, author and text the __init__ function would make sure they all exist when each instance is created.

[ ]:
class BlogEntry:

    def __init__(self, title):
        self.title = title
        self.author = None
        self.text = None

b = BlogEntry("First Blog Post")

Notice that the __init__ function does not need to supply meaningful values for the variables, just ensure they exist.

Class Design

When you use your own classes in a program it’s called Object Oriented Programming. In Python you have a choice to use OOP and most small programs won’t need classes of their own. In other languages, like Java, all programs must be witten using OOP. There are advantages and disadvantages of OOP. Whether or and how to best design classes is beyond the scope of this course but here are a few things that are important to know.

Ojbects are like packaged bundles, when they’re well designed they are easily shared with others. Classes bundle together code and variables. Other programmers assume they can call your functions but will avoid using your variables directly. Cooperation makes larger programs –written by hundreds or thousands of programmers– possible. There are many tricks and features of classes in Python. The most important ones we’ll learn today.

Inheritance

Classes are made to foster the sharing and reuse of code. You can extend the functionality of a class without altering its code with a mechnanism called inheritance.

Here’s an example of a class that builds on another class using inheritance.

[ ]:
class Base:

    def base_function(self):
        print ('Hello')


class Derived(Base):

    def derived_function(self):
        print ('World')


d = Derived()
d.base_function()
d.derived_function()

The class definitions look just like the ones from last week except:

class Derived(Base):

The Derived class asks to inherit the functions of Base. Later in the program you can see that an instance of Derived has the functions from both classes.

d.base_function()
d.derived_function()

Notice that the instance of Derived has function that it inherited from the base class Base.

Inheritance and Functions

Class inheritance lets you add or modify functions in the base class. Take a look at the code below. The Derived class has its own version of base_function and that version overrides the same method in the Base class.

Take a look:

[ ]:
class Base:

    def base_function(self):
        print ('Hello')


class Derived(Base):

    def base_function(self):
        print ('Override')

    def derived_function(self):
        print ('World')


d = Derived()
d.base_function()
d.derived_function()

The Derived class still has access to base_function and can call it using the super() function. Look closely at base_function in Derived.

[ ]:
class Base:

    def base_function(self):
        print ('Hello')


class Derived(Base):

    def base_function(self):
        super().base_function()
        print ('Override')

    def derived_function(self):
        print ('World')


d = Derived()
d.base_function()
d.derived_function()

There’s only one difference between this example and the previous example:

super().base_function()

The super() function returns an instance of the superclass (also known as the base class). If you use super() in any method of Derived you will get an instance of Base. You can use that instance to get any of the methods of Base that you need. There’s no restriction on when or what you call using super(). What would the output be if base_function in Derived was written this way?

def base_function(self):
    print ('Override')
    super().base_function()

Overriding __init__

Overriding __init__ is often necessary. The __init__ function initializes class data so when you create a derived class you almost always need to be sure that the variables in the base class are initialized.

[ ]:
class Base :

    def __init__(self) :
        print ('Initializing Base')
        self.base_var = 'Hello'

    def base_function(self) :
        print (self.base_var)


class Derived(Base) :

    def __init__(self) :
        print ('Initializing Derived')
        self.der_var = 'World'
        super().__init__()

    def derived_function(self) :
        print (self.der_var)


d = Derived()
d.base_function()
d.derived_function()

When to Use Inheritance

Class inheritance is a powerful tool and it has good and bad uses. Derived classes usually have an is-a relationship with their base class. That means the derived class “is a” base class.

Here’s what I mean:

[ ]:
class Animal:

    def __init__(self, num_legs):
        self.legs = num_legs

    def get_legs(self):
        return self.legs


class Cat(Animal):

    def __init__(self):
        super().__init__(4)

class Duck(Animal):

    def __init__(self):
        super().__init__(2)

c = Cat()
d = Duck()
print (f'A cat has {c.get_legs()} legs.')
print (f'A duck has {d.get_legs()} legs.')

This inheritance design works because a Duck and a Cat is-an Animal. Inheritance is appealing but there are other (often better) ways to mix the functions of two classes. It’s possible to have multiple inheritance in Python where a class inherits from multiple base classes.

Avoid using multiple inheritance!

Multiple inheritance makes classes too complicated (see the Diamond Problem of Death. It’s better to keep objects simple.

Mix-ins

Inheritance is useful but it can be confusing because base methods are called automatically. A mix-in is a way to get some of the functions of a base class but not all of them. There’s no automatic function calls in a mix-in so you have to do a bit more typing.

Here’s an example of a mix-in:

[ ]:
class Base:

    def base_function(self):
        print ('Hello')


class MixIn:

    def __init__(self) :
        self.base_instance = Base()

    def base_function(self) :
        self.base_instance.base_function()

    def derived_function(self) :
        print ('World')


m = MixIn()
m.base_function()
m.derived_function()

The mix-in works because MixIn contains an instance of Base. The MixIn class can use any member of Base in its own methods. Mix-ins give the programmer more control of what methods of Base. Mix-ins give you more control over what functions of Base are exposed at the expense of more code.