Chapter 17 Classes and methodsAlthough we are using some of Python’s object-oriented features, the programs from the last two chapters are not really object-oriented because they don’t represent the relationships between programmer-defined types and the functions that operate on them. The next step is to transform those functions into methods that make the relationships explicit. Code examples from this chapter are available from https://thinkpython.com/code/Time2.py, and solutions to the exercises are in https://thinkpython.com/code/Point2_soln.py. 17.1 Object-oriented featuresPython is an object-oriented programming language, which means that it provides features that support object-oriented programming, which has these defining characteristics:
For example, the Time class defined in Chapter 16 corresponds to the way people record the time of day, and the functions we defined correspond to the kinds of things people do with times. Similarly, the Point and Rectangle classes in Chapter 15 correspond to the mathematical concepts of a point and a rectangle. So far, we have not taken advantage of the features Python provides to support object-oriented programming. These features are not strictly necessary; most of them provide alternative syntax for things we have already done. But in many cases, the alternative is more concise and more accurately conveys the structure of the program. For example, in Time1.py there is no obvious connection between the class definition and the function definitions that follow. With some examination, it is apparent that every function takes at least one Time object as an argument. This observation is the motivation for methods; a method is a function that is associated with a particular class. We have seen methods for strings, lists, dictionaries and tuples. In this chapter, we will define methods for programmer-defined types. Methods are semantically the same as functions, but there are two syntactic differences:
In the next few sections, we will take the functions from the previous two chapters and transform them into methods. This transformation is purely mechanical; you can do it by following a sequence of steps. If you are comfortable converting from one form to another, you will be able to choose the best form for whatever you are doing. 17.2 Printing objectsIn Chapter 16, we defined a class named
Time and in Section 16.1, you
wrote a function named class Time: """Represents the time of day.""" def print_time(time): print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second)) To call this function, you have to pass a Time object as an argument: >>> start = Time() >>> start.hour = 9 >>> start.minute = 45 >>> start.second = 00 >>> print_time(start) 09:45:00
To make class Time: def print_time(time): print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second))
Now there are two ways to call >>> Time.print_time(start) 09:45:00
In this use of dot notation, Time is the name of the class,
and The second (and more concise) way is to use method syntax: >>> start.print_time() 09:45:00
In this use of dot notation, Inside the method, the subject is assigned to the first parameter, so in this case start is assigned to time. By convention, the first parameter of a method is
called self, so it would be more common to write
class Time: def print_time(self): print('%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)) The reason for this convention is an implicit metaphor:
This change in perspective might be more polite, but it is not obvious that it is useful. In the examples we have seen so far, it may not be. But sometimes shifting responsibility from the functions onto the objects makes it possible to write more versatile functions (or methods), and makes it easier to maintain and reuse code. As an exercise, rewrite 17.3 Another exampleHere’s a version of increment (from Section 16.3) rewritten as a method: # inside class Time: def increment(self, seconds): seconds += self.time_to_int() return int_to_time(seconds)
This version assumes that Here’s how you would invoke increment: >>> start.print_time() 09:45:00 >>> end = start.increment(1337) >>> end.print_time() 10:07:17 The subject, start, gets assigned to the first parameter, self. The argument, 1337, gets assigned to the second parameter, seconds. This mechanism can be confusing, especially if you make an error. For example, if you invoke increment with two arguments, you get: >>> end = start.increment(1337, 460) TypeError: increment() takes 2 positional arguments but 3 were given The error message is initially confusing, because there are only two arguments in parentheses. But the subject is also considered an argument, so all together that’s three. By the way, a positional argument is an argument that doesn’t have a parameter name; that is, it is not a keyword argument. In this function call: sketch(parrot, cage, dead=True) parrot and cage are positional, and dead is a keyword argument. 17.4 A more complicated exampleRewriting # inside class Time: def is_after(self, other): return self.time_to_int() > other.time_to_int() To use this method, you 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 English: “end is after start?” 17.5 The init methodThe init method (short for “initialization”) is
a special method that gets invoked when an object is instantiated.
Its full name is # inside class Time: def __init__(self, hour=0, minute=0, second=0): self.hour = hour self.minute = minute self.second = second
It is common for the parameters of self.hour = hour stores the value of the parameter hour as an attribute of self. The parameters are optional, so if you call Time with no arguments, you get the default values. >>> time = Time() >>> time.print_time() 00:00:00 If you provide one argument, it overrides hour: >>> time = Time (9) >>> time.print_time() 09:00:00 If you provide two arguments, they override hour and minute. >>> time = Time(9, 45) >>> time.print_time() 09:45:00 And if you provide three arguments, they override all three default values. As an exercise, write an init method for the Point class that takes x and y as optional parameters and assigns them to the corresponding attributes. 17.6 The __str__ method
For example, here is a str method for Time objects: # inside class Time: def __str__(self): return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second) When you print an object, Python invokes the str method: >>> time = Time(9, 45) >>> print(time) 09:45:00
When I write a new class, I almost always start by writing
As an exercise, write a str method for the Point class. Create a Point object and print it. 17.7 Operator overloadingBy defining other special methods, you can specify the behavior
of operators on programmer-defined types. For example, if you define
a method named Here is what the definition might look like: # inside class Time: def __add__(self, other): seconds = self.time_to_int() + other.time_to_int() return int_to_time(seconds) And here is how you could use it: >>> start = Time(9, 45) >>> duration = Time(1, 35) >>> print(start + duration) 11:20:00
When you apply the + operator to Time objects, Python invokes
Changing the behavior of an operator so that it works with
programmer-defined types is called operator overloading. For every
operator in Python there is a corresponding special method, like
As an exercise, write an add method for the Point class. 17.8 Type-based dispatchIn the previous section we added two Time objects, but you
also might want to add an integer to a Time object. The
following is a version of # inside class Time: def __add__(self, other): if isinstance(other, Time): return self.add_time(other) else: return self.increment(other) def add_time(self, other): seconds = self.time_to_int() + other.time_to_int() return int_to_time(seconds) def increment(self, seconds): seconds += self.time_to_int() return int_to_time(seconds) The built-in function isinstance takes a value and a class object, and returns True if the value is an instance of the class. If other is a Time object, Here are examples that use the + operator with different types: >>> start = Time(9, 45) >>> duration = Time(1, 35) >>> print(start + duration) 11:20:00 >>> print(start + 1337) 10:07:17 Unfortunately, this implementation of addition is not commutative. If the integer is the first operand, you get >>> print(1337 + start) TypeError: unsupported operand type(s) for +: 'int' and 'instance'
The problem is, instead of asking the Time object to add an integer,
Python is asking an integer to add a Time object, and it doesn’t know
how. But there is a clever solution for this problem: the
special method # inside class Time: def __radd__(self, other): return self.__add__(other) And here’s how it’s used: >>> print(1337 + start) 10:07:17 As an exercise, write an add method for Points that works with either a Point object or a tuple:
17.9 PolymorphismType-based dispatch is useful when it is necessary, but (fortunately) it is not always necessary. Often you can avoid it by writing functions that work correctly for arguments with different types. Many of the functions we wrote for strings also work for other sequence types. For example, in Section 11.2 we used histogram to count the number of times each letter appears in a word. def histogram(s): d = dict() for c in s: if c not in d: d[c] = 1 else: d[c] = d[c]+1 return d This function also works for lists, tuples, and even dictionaries, as long as the elements of s are hashable, so they can be used as keys in d. >>> t = ['spam', 'egg', 'spam', 'spam', 'bacon', 'spam'] >>> histogram(t) {'bacon': 1, 'egg': 1, 'spam': 4} Functions that work with several types are called polymorphic. Polymorphism can facilitate code reuse. For example, the built-in function sum, which adds the elements of a sequence, works as long as the elements of the sequence support addition. Since Time objects provide an add method, they work with sum: >>> t1 = Time(7, 43) >>> t2 = Time(7, 41) >>> t3 = Time(7, 37) >>> total = sum([t1, t2, t3]) >>> print(total) 23:01:00 In general, if all of the operations inside a function work with a given type, the function works with that type. The best kind of polymorphism is the unintentional kind, where you discover that a function you already wrote can be applied to a type you never planned for. 17.10 DebuggingIt is legal to add attributes to objects at any point in the execution of a program, but if you have objects with the same type that don’t have the same attributes, it is easy to make mistakes. It is considered a good idea to initialize all of an object’s attributes in the init method. If you are not sure whether an object has a particular attribute, you can use the built-in function hasattr (see Section 15.7). Another way to access attributes is the built-in function vars, which takes an object and returns a dictionary that maps from attribute names (as strings) to their values: >>> p = Point(3, 4) >>> vars(p) {'y': 4, 'x': 3} For purposes of debugging, you might find it useful to keep this function handy: def print_attributes(obj): for attr in vars(obj): print(attr, getattr(obj, attr))
The built-in function getattr takes an object and an attribute name (as a string) and returns the attribute’s value. 17.11 Interface and implementationOne of the goals of object-oriented design is to make software more maintainable, which means that you can keep the program working when other parts of the system change, and modify the program to meet new requirements. A design principle that helps achieve that goal is to keep interfaces separate from implementations. For objects, that means that the methods a class provides should not depend on how the attributes are represented. For example, in this chapter we developed a class that represents
a time of day. Methods provided by this class include
We could implement those methods in several ways. The details of the implementation depend on how we represent time. In this chapter, the attributes of a Time object are hour, minute, and second. As an alternative, we could replace these attributes with
a single integer representing the number of seconds
since midnight. This implementation would make some methods,
like After you deploy a new class, you might discover a better implementation. If other parts of the program are using your class, it might be time-consuming and error-prone to change the interface. But if you designed the interface carefully, you can change the implementation without changing the interface, which means that other parts of the program don’t have to change. 17.12 Glossary
17.13 ExercisesExercise 1 Download the code from this chapter from
https://thinkpython.com/code/Time2.py. Change the attributes of
Time to be a single integer representing seconds since
midnight. Then modify the methods (and the function
Exercise 2
This exercise is a cautionary tale about one of the most common, and difficult to find, errors in Python. Write a definition for a class named Kangaroo with the following methods:
Test your code by creating two Kangaroo objects, assigning them to variables named kanga and roo, and then adding roo to the contents of kanga’s pouch. Download https://thinkpython.com/code/BadKangaroo.py. It contains a solution to the previous problem with one big, nasty bug. Find and fix the bug. If you get stuck, you can download https://thinkpython.com/code/GoodKangaroo.py, which explains the problem and demonstrates a solution. |
ContributeIf you would like to make a contribution to support my books, you can use the button below and pay with PayPal. Thank you!
Are you using one of our books in a class?We'd like to know about it. Please consider filling out this short survey.
|