Chapter 15. Classes and Methods

Python is an object-oriented language—that is, it provides features that support object-oriented programming, which has these defining characteristics:

  • Most of the computation is expressed in terms of operations on objects.

  • Objects often represent things in the real world, and methods often correspond to the ways things in the real world interact.

  • Programs include class and method definitions.

For example, in the previous chapter we defined a Time class that corresponds to the way people record the time of day, and we defined functions that correspond to the kinds of things people do with times. But there was no explicit connection between the definition of the Time class and the function definitions that follow. We can make the connection explicit by rewriting a function as a method, which is defined inside a class definition.

Defining Methods

In the previous chapter, we defined a class named Time and wrote a function named print_time that displays a time of day:

class Time:
    """Represents the time of day."""

def print_time(time):
    s = f'{time.hour:02d}:{time.minute:02d}:{time.second:02d}'
    print(s)
       

To make print_time a method, all we have to do is move the function definition inside the class definition. Notice the change in indentation.

At the same time, we’ll change the name of the parameter from time to self. This change is not necessary, but it is conventional for the first parameter of a method to be named self:

class Time:
    """Represents the time of day."""    

    def print_time(self):
        s = f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}'
        print(s)
       

To call this function, you have to pass a Time object as an argument. Here’s the function we’ll use to make a Time object:

def make_time(hour, minute, second):
    time = Time()
    time.hour = hour
    time.minute = minute
    time.second = second
    return time
       

And here’s a Time instance:

start = make_time(9, 40, 0)
       

There are two ways to call print_time. The first (and less common) way is to use function syntax:

Time.print_time(start)
       
09:40:00
       

In this version, Time is the name of the class, print_time is the name of the method, and start is passed as a parameter. The second (and more idiomatic) way is to use the method syntax:

start.print_time()
       
09:40:00
       

In this version, start is the object the method is invoked on, which is called the receiver, based on the analogy that invoking a method is like sending a message to an object.

Regardless of the syntax, the behavior of the method is the same. The receiver is assigned to the first parameter, so inside the method, self refers to the same object as start.

Static Methods

As another example, let’s consider the int_to_time function. Here’s the version from Chapter 14:

def int_to_time(seconds):
    minute, second = divmod(seconds, 60)
    hour, minute = divmod(minute, 60)
    return make_time(hour, minute, second)
        

This function takes seconds as a parameter and returns a new Time object. If we transform it into a method of the Time class, we have to invoke it on a Time object. But if we’re trying to create a new Time object, what are we supposed to invoke it on?

We can solve this chicken-and-egg problem using a static method, which is a method that does not require an instance of the class to be invoked. Here’s how we rewrite this function as a static method:

%%add_method_to Time

    def int_to_time(seconds):
        minute, second = divmod(seconds, 60)
        hour, minute = divmod(minute, 60)
        return make_time(hour, minute, second)
        

Because it is a static method, it does not have self as a parameter. To invoke it, we use Time, which is the class object:

start = Time.int_to_time(34800)
        

The result is a new object that represents 9:40:

start.print_time()
        
09:40:00
        

Now that we have Time.from_seconds, we can use it to write add_time as a method. Here’s the function from the previous chapter:

def add_time(time, hours, minutes, seconds):
    duration = make_time(hours, minutes, seconds)
    seconds = time_to_int(time) + time_to_int(duration)
    return int_to_time(seconds)
        

And here’s a version rewritten as a method:

%%add_method_to Time

    def add_time(self, hours, minutes, seconds):
        duration = make_time(hours, minutes, seconds)
        seconds = time_to_int(self) + time_to_int(duration)
        return Time.int_to_time(seconds)
        

add_time has self as a parameter because it is not a static method. It is an ordinary method—also called an instance method. To invoke it, we need a Time instance:

end = start.add_time(1, 32, 0)
print_time(end)
        
11:12:00
        

Comparing Time Objects

As one more example, let’s write is_after as a method. Here’s the is_after function, which is a solution to an exercise in Chapter 14:

def is_after(t1, t2):
    return time_to_int(t1) > time_to_int(t2)
        

And here it is as a method:

%%add_method_to Time

    def is_after(self, other):
        return self.time_to_int() > other.time_to_int()
        

Because we’re comparing two objects, and the first parameter is self, we’ll call the second parameter other. To use this method, we have to invoke it on one object and pass the other as an argument:

end.is_after(start)
        
True
        

One nice thing about this syntax is that it almost reads like a question, “end is after start?”

The __str__ Method

When you write a method, you can choose almost any name you want. However, some names have special meanings. For example, if an object has a method named __str__, Python uses that method to convert the object to a string. For example, here is a __str__ method for a Time object:

%%add_method_to Time

    def __str__(self):
        s = f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}'
        return s
        

This method is similar to print_time from Chapter 14, except that it returns the string rather than printing it.

You can invoke this method in the usual way:

end.__str__()
        
'11:12:00'
        

But Python can also invoke it for you. If you use the built-in function str to convert a Time object to a string, Python uses the __str__ method in the Time class:

str(end)
        
'11:12:00'
        

And it does the same if you print a Time object:

print(end)
        
11:12:00
        

Methods like __str__ are called special methods. You can identify them because their names begin and end with two underscores.

Debugging

A Time object is valid if the values of minute and second are between 0 and 60—including 0 but not 60—and if hour is positive. Also, hour and minute should be integer values, but we might allow second to have a fraction part. Requirements like these are called invariants because they should always be true. To put it a different way, if they are not true, something has gone wrong.

Writing code to check invariants can help detect errors and find their causes. For example, you might have a method like is_valid that takes a Time object and returns False if it violates an invariant:

%%add_method_to Time

    def is_valid(self):
        if self.hour < 0 or self.minute < 0 or self.second < 0:
            return False
        if self.minute >= 60 or self.second >= 60:
            return False
        if not isinstance(self.hour, int):
            return False
        if not isinstance(self.minute, int):
            return False
        return True
        

Then, at the beginning of each method you can check the arguments to make sure they are valid:

%%add_method_to Time

    def is_after(self, other):
        assert self.is_valid(), 'self is not a valid Time'
        assert other.is_valid(), 'self is not a valid Time'
        return self.time_to_int() > other.time_to_int()
        

The assert statement evaluates the expression that follows. If the result is True, it does nothing; if the result is False, it causes an AssertionError. Here’s an example:

duration = Time(minute=132)
print(duration)
        
00:132:00
        
start.is_after(duration)
        
AssertionError: self is not a valid Time
        

assert statements are useful because they distinguish code that deals with normal conditions from code that checks for errors.

Glossary

object-oriented language: A language that provides features to support object-oriented programming, notably user-defined types and inheritance.

method: A function that is defined inside a class definition and is invoked on instances of that class.

receiver: The object a method is invoked on.

static method: A method that can be invoked without an object as receiver.

instance method: A method that must be invoked with an object as receiver.

special method: A method that changes the way operators and some functions work with an object.

operator overloading: The process of using special methods to change the way operators work with user-defined types.

invariant: A condition that should always be true during the execution of a program.

Exercises

Ask a Virtual Assistant

For more information about static methods, ask a virtual assistant:

  • “What’s the difference between an instance method and a static method?”

  • “Why are static methods called static?”

If you ask a virtual assistant to generate a static method, the result will probably begin with @staticmethod, which is a “decorator” that indicates that it is a static method. Decorators are not covered in this book, but if you are curious, you can ask a virtual assistant for more information.

In this chapter we rewrote several functions as methods. Virtual assistants are generally good at this kind of code transformation. As an example, paste the following function into a virtual assistant and ask it, “Rewrite this function as a method of the Time class.”

def subtract_time(t1, t2):
    return time_to_int(t1) - time_to_int(t2)
        

Exercise

In Chapter 14, a series of exercises asked you to write a Date class and several functions that work with Date objects. Now let’s practice rewriting those functions as methods:

  1. Write a definition for a Date class that represents a date—that is, a year, month, and day of the month.

  2. Write an __init__ method that takes year, month, and day as parameters and assigns the parameters to attributes. Create an object that represents June 22, 1933.

  3. Write a __str__ method that uses a format string to format the attributes and returns the result. If you test it with the Date you created, the result should be 1933-06-22.

  4. Write a method called is_after that takes two Date objects and returns True if the first comes after the second. Create a second object that represents September 17, 1933, and check whether it comes after the first object.

Hint: you might find it useful to write a method called to_tuple that returns a tuple that contains the attributes of a Date object in year-month-day order.