Classes and Objects

Quiz: counts toward finishing the lesson. Check: just for practice.

Introduction

Here's a simple example class:

The class keyword defines a class. The name of a class should start with a capital letter. A class should have a docstring that describes its purpose and anything else someone needs to know to use it. Functions that are part of a class are called methods. This Pet class has a single method with the special name __init__ (two underscores on either side). It runs when a Pet is created and stores the species and name you passed in. The species and name are the data members (also called fields or attributes) of the Pet class. The special parameter self refers to the object being created. You don't call __init__ by name. Instead you use the name of the class. You also don't pass an argument for self, Python does that for you. Confused? Some examples should help. Let's create a couple of Pets:

pet_1 = Pet("capybara", "Beatrice")
pet_2 = Pet("kangaroo", "Joey")

Each of these lines creates a new Pet object. The Pet class is a general blueprint for pets. To create a specific Pet object, we use the name of the class as shown above, which automatically calls __init__. An object is also called an "instance" of a class, and creating an object is called "instantiating" the class. Once we've created an object, we can access its attributes using dot notation:

print(pet_1.species)
print(pet_1.name)

We also use dot notation to call an object's methods, but our Pet class has none besides __init__, so let's look at another example:

Notice that the area and perimeter methods have only the self parameter. They don't need width or height passed in, because those are already part of the object and can be reached through self. Now that we've defined the Rectangle class, here's an example of using it:

How do you know what to put in a class?

When designing a class, we decide which features of the thing we're representing matter for our purposes. That tells us what data and functions the class needs. For example, if we're concerned with selling cars, price is a relevant feature. If we're only concerned with driving cars, it isn't.

"Private" class members

Some languages have a keyword (private in Java and C++) that stops code outside a class from accessing a data member or method. Methods of the class can still use it. Python has no such keyword. Instead, it has a convention: a data member or method whose name begins with an underscore should be treated as private, even though Python doesn't enforce it.

Here's an example of a bank account class that has private data members:

Notice that for class names, instead of separating words with underscores ("snake case"), we follow Python convention and start each new word with a capital letter ("CapWords", sometimes called "Pascal case"). Here's an example of using the BankAccount class:

If we want other code to be able to access a private data member, we provide get and/or set methods. A get method returns the current value of a data member. A set method takes a parameter and stores it in the data member. The usual naming convention is get_ or set_ followed by the name of the data member. For the account ID we provided both. For the balance we provided only a get method, since the balance changes through the deposit and withdraw methods.

Check your understanding. Predict the result. Each object keeps its own count.

class Counter:
    def __init__(self, start):
        self._count = start

    def add(self, amount):
        self._count += amount

    def get_count(self):
        return self._count

a = Counter(10)
b = Counter(10)
a.add(5)
b.add(1)
a.add(b.get_count())
x = a.get_count()
y = b.get_count()

Predict what each variable holds after the program finishes. The program is not run for you. Change the values below to your predictions:

  • final_x is the value of x at the end
  • final_y is the value of y at the end
Hint 1

Each object has its own _count, and a method only changes the object it is called on.

You might wonder why we would use private data members if it means writing more methods. The reason is that it lets us control access. If we don't want a data member to be accessed, we don't write get or set methods for it. If a data member should only hold certain values, the set method can check for that. If a data member is public, it can be changed to anything from anywhere, which makes your program harder to understand and debug. Controlling access this way is called information hiding (or data hiding).

Information hiding also separates the interface from the implementation. The interface is the names and parameters of the public functions. The implementation is the code inside those functions, plus any private data members and private functions. If users' code reaches into the implementation, it comes to depend on those details, and changing the implementation later will break it. With a defined interface, the implementation can change as long as the interface stays the same.

Methods can be private in the same way. A method should be private if it is only for internal use by the class and not part of how we expect users to work with it. For example, suppose a Fraction class keeps every fraction in reduced form. It could have a greatest_common_divisor() method for its own use. We wouldn't expect users to ask a Fraction for the gcd of two integers. We expect them to do Fraction-like things, such as printing a Fraction or multiplying it by another. Finding the gcd is an implementation detail that can be hidden from users.

Everything is an object

In Python, everything is an object. Strings, integers, and all other values are objects. Functions are objects. Even the special value None is an object. Object-oriented design is a useful way to organize code. It can feel abstract at first, but it is worth learning.

Printing objects of user-defined classes

If you try to print an object of a user-defined class, you'll get something like this:

print(account_1)
<__main__.BankAccount object at 0x103506d30>

By default, Python only shows which class the object belongs to and its address in memory. To print the values of the data members, access them in the print statement, as shown earlier. There are ways to customize how an object prints, but that is for a future lesson.

Quizzes

  1. Define a class named HourlyWorker that has three private data members: _name, _ID, and _wage. The class should have a docstring and an __init__ method. The parameters to the __init__ method should be in the order listed above.
Hint 1

The __init__ method takes self, name, ID, and wage, and assigns each one to a data member whose name starts with an underscore.

  1. Define a class named Box that has three private data members: _length, _width, and _height. The class should have a docstring and an __init__ method. The parameters to the __init__ method should be in the order listed above. The class should also have a method named volume that returns the volume of the Box, and a method named surface_area that returns the surface area of the Box.
Hint 1

Volume is length times width times height. Surface area is 2 * (length*width + length*height + width*height).

  1. Predict the result. Notice which methods change the object and which only return a value.
class Tank:
    def __init__(self, level):
        self._level = level

    def fill(self, amount):
        self._level += amount

    def level_after(self, amount):
        return self._level + amount

    def get_level(self):
        return self._level

t1 = Tank(20)
t2 = Tank(5)
t1.level_after(10)
t2.fill(t1.get_level())
x = t1.get_level()
y = t2.get_level()

Predict what each variable holds after the program finishes. The program is not run for you. Change the values below to your predictions:

  • final_x is the value of x at the end
  • final_y is the value of y at the end
Hint 1

A method that only returns a value leaves the data member as it was.

Projects

Practice what you learned in your own editor. Each project has its own page.