9 Objects

Object-oriented programming has become a mainstream, or even the mainstream, way of approaching programming. The idea is a simple one: instead of defining our functions in one part of the code, and the data on which those functions operate in a separate part of the code, we define them together.

Or, to put it in terms of language, in traditional, procedural programming, we write a list of nouns (data) and a separate list of verbs (functions), leaving it up to the programmer to figure out which goes with which. In object-oriented programming, the verbs (functions) are defined along with the nouns (data), helping us to know what goes with what.

In the world of object-oriented programming, each noun is an object. We say that each object has a type, or a class, to which it belongs. And the verbs (functions) we can invoke on each object are known as methods.

For an example of traditional, procedural programming versus object-oriented programming, consider how we could calculate a student’s final grade, based on the average of their test scores. In procedural programming, we’d make sure the grades were in a list of integers and then write an average function that returned the arithmetic mean:

def average(numbers):
    return sum(numbers) / len(numbers)
 
scores = [85, 95, 98, 87, 80, 92]
print(f'The final score is {average(scores)}.')

This code works, and works reliably. But the caller is responsible for keeping track of the numbers as a list ... and for knowing that we have to call the average method ... and for combining them in the right way.

In the object-oriented world, we would approach the problem by creating a new data type, which we might call a ScoreList. We would then create a new instance of ScoreList.

Even if it’s the same data underneath, a ScoreList is more explicitly and specifically connected to our domain than a generic Python list. We could then invoke the appropriate method on the ScoreList object:

class ScoreList():
    def __init__(self, scores):
        self.scores = scores
 
    def average(self):
        return sum(self.scores) / len(self.scores)
 
scores = ScoreList([85, 95, 98, 87, 80, 92])
print(f'The final score is {scores.average()}.')

As you can see, there’s no difference from the procedural method in what’s actually being calculated, and even what technique we’re using to calculate it. But there’s an organizational and semantic difference here, one that allows us to think in a different way.

We’re now thinking at a higher level of abstraction and can better reason about our code. Defining our own types also allows us to use shorthand when describing concepts. Consider the difference between telling someone that you bought a “bookshelf” and describing “wooden boards held together with nails and screws, stored upright and containing places for storing books.” The former is shorter, less ambiguous, and more semantically powerful than the latter.

Another advantage is that if we decide to calculate the average in a new way--for example, some teachers might drop the lowest score--then we can keep the existing interface while modifying the underlying implementation.

So, what are the main reasons for using object-oriented techniques?

  • We can organize our code into distinct objects, each of which handles a different aspect of our code. This makes for easier planning and maintenance, as well as allowing us to divide a project among multiple people.

  • We can create hierarchies of classes, with each child in the hierarchy inheriting functionality from its parents. This reduces the amount of code we need to write and simultaneously reinforces the relationships among similar data types. Given that many classes are slight modifications of other ones, this saves time and coding.

  • By creating data types that work the same way as Python’s built-in types, our code feels like a natural extension to the language, rather than bolted on. Moreover, learning how to use a new class requires learning only a tiny bit of syntax, so you can concentrate on the underlying ideas and functionality.

  • While Python doesn’t hide code or make it private, you’re still likely to hear about the difference between an object’s implementation and its interface. If I’m using an object, then I care about its interface--that is, the methods that I can call on it and what they do. How the object is implemented internally is not a priority for me and doesn’t affect my day-to-day work. This way, I can concentrate on the coding I want to do, rather than the internals of the class I’m using, taking advantage of the abstraction that I’ve created via the class.

Object-oriented programming isn’t a panacea; over the years, we’ve found that, as with all other paradigms, it has both advantages and disadvantages. For example, it’s easy to create monstrously large objects with huge numbers of methods, effectively creating a procedural system disguised as an object-oriented one. It’s possible to abuse inheritance, creating hierarchies that make no sense. And by breaking the system into many small pieces, there’s the problem of testing and integrating those pieces, with so many possible lines of communication.

Nevertheless, the object paradigm has helped numerous programmers to modularize their code, to focus on specific aspects of the program on which they’re working, and to exchange data with objects written by other people.

In Python, we love to say that “everything is an object.” At its heart, this means that the language is consistent; the types (such as str and dict) that come with the language are defined as classes, with methods. Our objects work just like the built-in objects, reducing the learning curve for both those implementing new classes and those using them.

Consider that when you learn a foreign language, you discover that nouns and verbs have all sorts of rules. But then there are the inevitable inconsistencies and exceptions to those rules. By having one consistent set of rules for all objects, Python removes those frustrations for non-native speakers--giving us, for lack of a better term, the Esperanto of programming languages. Once you’ve learned a rule, you can apply it throughout the language.

Note One of the hallmarks of Python is its consistency. Once you learn a rule, it applies to the entire language, with no exceptions. If you understand variable lookup (LEGB, described in chapter 6) and attribute lookup (ICPO, described later in this chapter), you’ll know the rules that Python applies all of the time, to all objects, without exception--both those that you create and those that come baked into the language.

At the same time, Python doesn’t force you to write everything in an object-oriented style. Indeed, it’s common to combine paradigms in Python programs, using an amalgam of procedural, functional, and object-oriented styles. Which style you choose, and where, is left up to you. But at the end of the day, even if you’re not writing in an object-oriented style, you’re still using Python’s objects.

If you’re going to code in Python, you should understand Python’s object system--the ways objects are created, how classes are defined and interact with their parents, and how we can influence the ways classes interact with the rest of the world. Even if you write in a procedural style, you’ll still be using classes defined by other people, and knowing how those classes work will make your coding easier and more straightforward.

This chapter contains exercises aimed at helping you to feel more comfortable with Python’s objects. As you go through these exercises, you’ll create classes and methods, create attributes at the object and class levels, and work with such concepts as composition and inheritance. When you’re done, you’ll be prepared to create and work with Python objects, and thus both write and maintain Python code.

Note The previous chapter, about modules, was short and simple. This chapter is the opposite--long, with many important ideas that can take some time to absorb. This chapter will take time to get through, but it’s worth the effort. Understanding object-oriented programming won’t just help you in writing your own classes; it’ll also help you to understand how Python itself is built, and how the built-in types work.

Table 9.1 What you need to know

Concept

What is it?

Example

To learn more

class

Keyword for creating Python classes

class Foo

http://mng.bz/1zAV

__init__

Method invoked automatically when a new instance is created

def __init__(self):

http://mng.bz/PAa9

__repr__

Method that returns a string containing an object’s printed representation

def __repr__(self):

http://mng.bz/Jyv0

super built-in

Returns a proxy object on which methods can be invoked; typically used to invoke a method on a parent class

super().__init__()

http://mng.bz/wB0q

dataclasses .dataclass

A decorator that simplifies the definition of classes

@dataclass

http://mng.bz/qMew

Exercise 38 Ice cream scoop

If you’re going to be programming with objects, then you’ll be creating classes--lots of classes. Each class should represent one type of object and its behavior. You can think of a class as a factory for creating objects of that type--so a Car class would create cars, also known as “car objects” or “instances of Car.” Your beat-up sedan would be a car object, as would a fancy new luxury SUV.

In this exercise, you’ll define a class, Scoop, that represents a single scoop of ice cream. Each scoop should have a single attribute, flavor, a string that you can initialize when you create the instance of Scoop.

Once your class is created, write a function (create_scoops) that creates three instances of the Scoop class, each of which has a different flavor (figure 9.1). Put these three instances into a list called scoops (figure 9.2). Finally, iterate over your scoops list, printing the flavor of each scoop of ice cream you’ve created.

Figure 9.1 Three instances of Scoop, each referring to its class

Figure 9.2 Our three instances of Scoop in a list

Working it out

The key to understanding objects in Python--and much of the Python language--is attributes. Every object has a type and one or more attributes. Python itself defines some of these attributes; you can identify them by the __ (often known as dunder in the Python world) at the beginning and end of the attribute names, such as __name__ or __init__.

When we define a new class, we do so with the class keyword. We then name the class (Scoop, in this case) and indicate, in parentheses, the class or classes from which our new class inherits.

Our __init__ method is invoked after the new instance of Scoop has been created, but before it has been returned to whoever invoked Scoop('flavor'). The new object is passed to __init__ in self (i.e., the first parameter), along with whatever arguments were passed to Scoop(). We thus assign self.flavor = flavor, creating the flavor attribute on the new instance, with the value of the flavor parameter.

Talking about your “self”

The first parameter in every method is traditionally called self. However, self isn’t a reserved word in Python; the use of that word is a convention and comes from the Smalltalk language, whose object system influenced Python’s design.

In many languages, the current object is known as this. Moreover, in such languages, this isn’t a parameter, but rather a special word that refers to the current object. Python doesn’t have any such special word; the instance on which the method was invoked will always be known as self, and self will always be the first parameter in every method.

In theory, you can use any name you want for that first parameter, including this. (But, really, what self-respecting language would do so?) Although your program will still work, all Python developers and tools assume that the first parameter, representing the instance, will be called self, so you should do so too.

Just as with regular Python functions, there isn’t any enforcement of types here. The assumption is that flavor will contain a str value because the documentation will indicate that this is what it expects.

Note If you want to enforce things more strictly, then consider using Python’s type annotations and Mypy or a similar type-checking tool. You can find more information about Mypy at http://mypy-lang.org/. Also, you can find an excellent introduction to Python’s type annotations and how to use them at http://mng.bz/mByr.

To create three scoops, I use a list comprehension, iterating over the flavors and creating new instances of Scoop. The result is a list with three Scoop objects in it, each with a separate flavor:

scoops = [Scoop(flavor)
          for flavor in ('chocolate', 'vanilla', 'persimmon')]

If you’re used to working with objects in another programming language, you might be wondering where the “getter” and “setter” methods are, to retrieve and set the value of the flavor attribute. In Python, because everything is public, there’s no real need for getters and setters. And indeed, unless you have a really good reason for it, you should probably avoid writing them.

Note If and when you find yourself needing a getter or setter, you might want to consider a Python property, which hides a method call behind the API of an attribute change or retrieval. You can learn more about properties here: http://mng.bz/5aWB.

I should note that even our simple Scoop class exhibits several things that are common to nearly all Python classes. We have an __init__ method, whose parameters allow us to set attributes on newly created instances. It stores state inside self, and it can store any type of Python object in this way--not just strings or numbers, but also lists and dicts, as well as other types of objects.

Note Don’t make persimmon ice cream. Your family will never let you forget it.

Solution

class Scoop():
    def __init__(self, flavor):    
        self.flavor = flavor       
 
 
def create_scoops():
    scoops = [Scoop('chocolate'),
              Scoop('vanilla'),
              Scoop('persimmon')]
    for scoop in scoops:
        print(scoop.flavor)
 
create_scoops()

Every method’s first parameter is always going to be “self,” representing the current instance.

Sets the “flavor” attribute to the value in the parameter “flavor”

You can work through a version of this code in the Python Tutor at http://mng.bz/ 8pMZ.

Screencast solution

Watch this short video walkthrough of the solution: https://livebook.manning.com/ video/python-workout.

Beyond the exercise

If you’re coding in Python, you’ll likely end up writing classes on a regular basis. And if you’re doing that, you’ll be writing many __init__ methods that add attributes to objects of various sorts. Here are some additional, simple classes that you can write to practice doing so:

  • Write a Beverage class whose instances will represent beverages. Each beverage should have two attributes: a name (describing the beverage) and a temperature. Create several beverages and check that their names and temperatures are all handled correctly.

  • Modify the Beverage class, such that you can create a new instance specifying the name, and not the temperature. If you do this, then the temperature should have a default value of 75 degrees Celsius. Create several beverages and double-check that the temperature has this default when not specified.

  • Create a new LogFile class that expects to be initialized with a filename. Inside of __init__, open the file for writing and assign it to an attribute, file, that sits on the instance. Check that it’s possible to write to the file via the file attribute.

What does __init__ do?

A simple class in Python looks like this:

class Foo():
    def __init__(self, x):
        self.x = x

And sure enough, with the Foo class in place, we can say

f = Foo(10)
print(f.x)

This leads many people, and particularly those who come from other languages, to call __init__ a constructor, meaning the method that actually creates a new instance of Foo. But that’s not quite the case.

When we call Foo(10), Python first looks for the Foo identifier in the same way as it looks for every other variable in the language, following the LEGB rule. It finds Foo as a globally defined variable, referencing a class. Classes are callable, meaning that they can be invoked with parentheses. And thus, when we ask to invoke it and pass 10 as an argument, Python agrees.

But what actually executes? The constructor method, of course, which is known as __new__. Now, you should almost never implement __new__ on your own; there are some cases in which it might be useful, but in the overwhelming majority of cases, you don’t want to touch or redefine it. That’s because __new__ creates the new object, something we don't want to have to deal with.

The __new__ method also returns the newly created instance of Foo to the caller. But before it does that, it does one more thing: it looks for, and then invokes, the __init__ method. This means that __init__ is called after the object is created but before it’s returned.

And what does __init__ do? Put simply, it adds new attributes to the object.

Whereas other programming languages talk about “instance variables” and “class variables,” Python developers have only one tool, namely the attribute. Whenever you have a.b in code, we can say that b is an attribute of a, meaning (more or less) that b references an object associated with a. You can think of the attributes of an object as its own private dict.

The job of __init__ is thus to add one or more attributes to our new instance. Unlike languages such as C# and Java, we don’t just declare attributes in Python; we must actually create and assign to them, at runtime, when the new instance is created.

In all Python methods, the self parameter refers to the instance. Any attributes we add to self will stick around after the method returns. And so it’s natural, and thus preferred, to assign a bunch of attributes to self in __init__.

Let’s see how this works, step by step. First, let’s define a simple Person class, which assigns a name to the object:

class Person:
    def __init__(self, name):
        self.name = name

Then, let’s create a new instance of Person:

p = Person('myname')

What happens inside of Python? First, the __new__ method, which we never define, runs behind the scenes, creating the object, as shown in figure 9.3.

Figure 9.3 When we create an object, __new__ is invoked.

It creates a new instance of Person and holds onto it as a local variable. But then __new__ calls __init__. It passes the newly created object as the first argument to __init__, then it passes all additional arguments using *args and **kwargs, as shown in figure 9.4.

__Figure 9.4 new__ then calls __init__.

Now __init__ adds one or more attributes to the new object, as shown in figure 9.5, which it knows as self, a local variable.

Figure 9.5 __init__ adds attributes to the object.

Finally, __new__ returns the newly created object to its caller, with the attribute that was added, as shown in figure 9.6.

Figure 9.6 Finally, __init__ exits, and the object in __new__ is returned to the caller.

Now, could we add new attributes to our instance after __init__ has run? Yes, absolutely--there’s no technical barrier to doing that. But as a general rule, you want to define all of your attributes in __init__ to ensure that your code is as readable and obvious as possible. You can modify the values later on, in other methods, but the initial definition should really be in __init__.

Notice, finally, that __init__ doesn’t use the return keyword. That’s because its return value is ignored and doesn’t matter. The point of __init__ lies in modifying the new instance by adding attributes, not in yielding a return value. Once __init__ is done, it exits, leaving __new__ with an updated and modified object. __new__ then returns this new object to its caller.

Exercise 39 Ice cream bowl

Whenever I teach object-oriented programming, I encounter people who’ve learned it before and are convinced that the most important technique is inheritance. Now, inheritance is certainly important, and we’ll look into it momentarily, but a more important technique is composition, when one object contains another object.

Calling it a technique in Python is a bit overblown, since everything is an object, and we can assign objects to attributes. So having one object owned by another object is just ... well, it’s just the way that we connect objects together.

That said, composition is also an important technique, because it lets us create larger objects out of smaller ones. I can create a car out of a motor, wheels, tires, gearshift, seats, and the like. I can create a house out of walls, floors, doors, and so forth. Dividing a project up into smaller parts, defining classes that describe those parts, and then joining them together to create larger objects--that’s how object-oriented programming works.

In this exercise, we’re going to see a small-scale version of that. In the previous exercise, we created a Scoop class that represents one scoop of ice cream. If we’re really going to model the real world, though, we should have another object into which we can put the scoops. I thus want you to create a Bowl class, representing a bowl into which we can put our ice cream (figure 9.7); for example

s1 = Scoop('chocolate')
s2 = Scoop('vanilla')
s3 = Scoop('persimmon')
 
b = Bowl()
b.add_scoops(s1, s2)
b.add_scoops(s3)
print(b)

Figure 9.7 A new instance of Bowl, with an empty list of scoops

The result of running print(b) should be to display the three ice cream flavors in our bowl (figure 9.8). Note that it should be possible to add any number of scoops to the bowl using Bowl.add_scoops.

Figure 9.8 Three Scoop objects in our bowl

Working it out

The solution doesn’t involve any changes to our Scoop class. Rather, we create our Bowl such that it can contain any number of instances of Scoop.

First of all, we define the attribute self.scoops on our object to be a list. We could theoretically use a dict or a set, but given that there aren’t any obvious candidates for keys, and that we might want to preserve the order of the scoops, I’d argue that a list is a more logical choice.

Remember that we’re storing instances of Scoop in self.scoops. We aren’t just storing the string that describes the flavors. Each instance of Scoop will have its own flavor attribute, a string containing the current scoop’s flavor.

We create the self.scoops attribute, as an empty list, in __init__.

Then we need to define add_scoops, which can take any number of arguments--which we’ll assume are instances of Scoop--and add them to the bowl. This means, almost by definition, that we’ll need to use the splat operator (*) when defining our *new_scoops parameter. As a result, new_scoops will be a tuple containing all of the arguments that were passed to add_scoops.

Note There’s a world of difference between the variable new_scoops and the attribute self.scoops. The former is a local variable in the function, referring to the tuple of Scoop objects that the user passed to add_scoops. The latter is an attribute, attached to the self local variable, that refers to the object instance on which we’re currently working.

We can then iterate over each element of scoops, adding it to the self.scoops attribute. We do this in a for loop, invoking list.append on each scoop.

Finally, to print the scoops, we simply invoke print(b). This has the effect of calling the __repr__ method on our object, assuming that one is defined. Our __repr__ method does little more than invoke str.join on the strings that we extract from the flavors.

repr vs. str

You can define __repr__, __str__, or both on your objects. In theory, __repr__ produces strings that are meant for developers and are legitimate Python syntax. By contrast, __str__ is how your object should appear to end users.

In practice, I tend to define __repr__ and ignore __str__. That’s because __repr__ covers both cases, which is just fine if I want all string representations to be equivalent. If and when I want to distinguish between the string output produced for developers and that produced for end users, I can always add a __str__ later on.

In this book, I’m going to use __repr__ exclusively. But if you want to use __str__, that’s fine--and it’ll be more officially correct to boot.

Notice, however, that we’re not invoking str.join on a list comprehension, because there are no square brackets. Rather, we’re invoking it on a generator expression, which you can think of as a lazy-evaluating version of a list comprehension. True, in a case like this, there’s really no performance benefit. My point in using it was to demonstrate that nearly anywhere you can use a list comprehension, you can use a generator expression instead.

is-a vs. has-a

If you have any experience with object-oriented programming, then you might have been tempted to say here that Scoop inherits from Bowl, or that Bowl inherits from Scoop. Neither is true, because inheritance (which we’ll explore later in this chapter) describes a relationship known in computer science as “is-a.” We can say that an employee is-a person, or that a car is-a vehicle, which would point to such a relationship.

In real life, we can say that a bowl contains one or more scoops. In programming terms, we’d describe this as Bowl has-a Scoop. The “has-a” relationship doesn’t describe inheritance, but rather composition.

I’ve found that relative newcomers to object-oriented programming are often convinced that if two classes are involved, one of them should probably inherit from the other. Pointing out the “is-a” rule for inheritance, versus the “has-a” rule for composition, helps to clarify the two different relationships and when it’s appropriate to use inheritance versus composition.

Solution

class Scoop():
    def __init__(self, flavor):
        self.flavor = flavor
 
class Bowl():
    def __init__(self):
        self.scoops = []                                   
 
    def add_scoops(self, *new_scoops):                     
        for one_scoop in new_scoops:
            self.scoops.append(one_scoop)
 
    def __repr__(self):
        return '
'.join(s.flavor for s in self.scoops)    
 
s1 = Scoop('chocolate')
s2 = Scoop('vanilla')
s3 = Scoop('persimmon')
 
b = Bowl()
b.add_scoops(s1, s2)
b.add_scoops(s3)
print(b)

Initializes self.scoops with an empty list

*new_scoops is just like *args. You can use whatever name you want.

Creates a string via str.join and a generator expression

You can work through a version of this code in the Python Tutor at http://mng.bz/EdWo.

Screencast solution

Watch this short video walkthrough of the solution: https://livebook.manning.com/ video/python-workout.

Beyond the exercise

You’ve now seen how to create an explicit “has-a” relationship between two classes. Here are some more opportunities to explore this type of relationship:

  • Create a Book class that lets you create books with a title, author, and price. Then create a Shelf class, onto which you can place one or more books with an add_book method. Finally, add a total_price method to the Shelf class, which will total the prices of the books on the shelf.

  • Write a method, Shelf.has_book, that takes a single string argument and returns True or False, depending on whether a book with the named title exists on the shelf.

  • Modify your Book class such that it adds another attribute, width. Then add a width attribute to each instance of Shelf. When add_book tries to add books whose combined widths will be too much for the shelf, raise an exception.

Reducing redundancy with dataclass

Do you feel like your class definitions repeat themselves? If so, you’re not alone. One of the most common complaints I hear from people regarding Python classes is that the __init__ method basically does the same thing in each class: taking arguments and assigning them to attributes on self.

As of Python 3.7, you can cut out some of the boilerplate class-creation code with the dataclass decorator, focusing on the code you actually want to write. For example, here’s how the Scoop class would be defined:

@dataclass
class Scoop():
    flavor : str

Look, there’s no __init__ method! You don’t need it here; the @dataclass decorator used writes it for you. It also takes care of other things, such as comparisons and a better version of __repr__. Basically, the whole point of data classes is to reduce your workload.

Notice that we used a type annotation (str) to indicate that our flavor attribute should only take strings. Type annotations are normally optional in Python, but if you’re declaring attributes in a data class, then they’re mandatory. Python, as usual, ignores these type annotations; as mentioned earlier in this chapter, type checking is done by external programs such as Mypy.

Also notice that we define flavor at the class level, even though we want it to be an attribute on our instances. Given that you almost certainly don’t want to have the same attribute on both instances and classes, this is fine; the dataclass decorator will see the attribute, along with its type annotation, and will handle things appropriately.

How about our Bowl class? How could we define it with a data class? It turns out that we need to provide a bit more information:

from typing import List
from dataclasses import dataclass, field
 
@dataclass
class Bowl():
    scoops: List[Scoop] = field(default_factory=list)
 
    def add_scoops(self, *new_scoops):
        for one_scoop in new_scoops:
            self.scoops.append(one_scoop)
 
    def __repr__(self):
        return '
'.join(s.flavor for s in self.scoops)

Let’s ignore the methods add_scoops and __repr__ and concentrate on the start of our class. First, we again use the @dataclass decorator. But then, when we define our scoops attribute, we give not just a type but a default value.

Notice that the type that we provide, List[int], has a capital “L”. This means that it’s distinct from the built-in list type. It comes from the typing module, which comes with Python and provides us with objects meant for use in type annotations. The List type, when used by itself, represents a list of any type. But when combined with square brackets, we can indicate that all elements of the list scoops will be objects of type Scoop.

Normally, default values can just be assigned to their attributes. But because scoops is a list, and thus mutable, we need to get a little fancier. When we create a new instance of Bowl, we don’t want to get a reference to an existing object. Rather, we want to invoke list, returning a new instance of list and assigning it to scoops. To do this, we need to use default_factory, which tells dataclass that it shouldn’t reuse existing objects, but should rather create new ones.

This book uses the classic, standard way of defining Python classes--partly to support people still using Python 3.6, and partly so that you can understand what’s happening under the hood. But I wouldn’t be surprised if dataclass eventually becomes the default way to create Python classes, and if you want to use them in your solutions, you should feel free to do so.

How Python searches for attributes

In chapter 6, I discussed how Python searches for variables using LEGB--first searching in the local scope, then enclosing, then global, and finally in the builtins namespace. Python adheres to this rule consistently, and knowing that makes it easier to reason about the language.

Python similarly searches for attributes along a standard, well-defined path. But that path is quite different from the LEGB rule for variables. I call it ICPO, short for “instance, class, parents, and object.” I’ll explain how that works.

When you ask Python for a.b, it first asks the a object whether it has an attribute named b. If so, then the value associated with a.b is returned, and that’s the end of the process. That’s the “I” of ICPO--we first check on the instance.

But if a doesn’t have a b attribute, then Python doesn’t give up. Rather, it checks on a’s class, whatever it is. Meaning that if a.b doesn’t exist, we look for type(a).b. If that exists, then we get the value back, and the search ends. That’s the “C” of ICPO.

Right away, this mechanism explains why and how methods are defined on classes, and yet can be called via the instance. Consider the following code:

s = 'abcd'
print(s.upper())

Here, we define s to be a string. We then invoke s.upper. Python asks s if it has an attribute upper, and the answer is no. It then asks if str has an attribute upper, and the answer is yes. The method object is retrieved from str and is then invoked. At the same time, we can talk about the method as str.upper because it is indeed defined on str, and is eventually located there.

What if Python can’t find the attribute on the instance or the class? It then starts to check on the class’s parents. Until now, we haven’t really seen any use of that; all of our classes have automatically and implicitly inherited from object. But a class can inherit from any other class--and this is often a good idea, since the subclass can take advantage of the parent class’s functionality.

Here’s an example:

class Foo():
    def __init__(self, x):
        self.x = x
    def x2(self):
        return self.x * 2
 

class Bar(Foo):
    def x3(self):
        return self.x * 3
 

b = Bar(10)
 
print(b.x2())     
print(b.x3())     

Prints 20

Prints 30

In this code, we create an instance of Bar, a class that inherits from Foo (figure 9.9).

Figure 9.9 Bar inherits from Foo, which inherits from object.

Figure 9.10 b is an instance of Bar.

When we create the instance of Bar, Python looks for __init__. Where? First on the instance, but it isn’t there. Then on the class (Bar), but it isn’t there. Then it looks at Bar’s parent, Foo, and it finds __init__ there. That method runs, setting the attribute x, and then returns, giving us b, an instance of Bar with x equal to 10 (figure 9.10).

The same thing happens when we invoke x2. We look on b and can’t find that method. We then look on type(b), or Bar, and can’t find the method. But when we check on Bar’s parent, Foo, we find it, and that method executes. If we had defined a method of our own named x2 on Bar, then that would have executed instead of Foo.x2.

Finally, we invoke x3. We check on b and don’t find it. We check on Bar and do find it, and that method thus executes.

What if, during our ICPO search, the attribute doesn’t exist on the instance, class, or parent? We then turn to the ultimate parent in all of Python, object. You can create an instance of object, but there’s no point in doing so; it exists solely so that other classes can inherit from it, and thus get to its methods.

As a result, if you don’t define an __init__ method, then object.__init__ will run. And if you don’t define __repr__, then object.__repr__ will run, and so forth.

The final thing to remember with the ICPO search path is that the first match wins. This means that if two attributes on the search path have the same name, Python won’t ever find the later one. This is normally a good thing in that it allows us to override methods in subclasses. But if you’re not expecting that to happen, then you might end up being surprised.

Exercise 40 Bowl limits

We can add an attribute to just about any object in Python. When writing classes, it’s typical and traditional to define data attributes on instances and method attributes on classes. But there’s no reason why we can’t define data attributes on classes too.

In this exercise, I want you to define a class attribute that will function like a constant, ensuring that we don’t need to hardcode any values in our class.

What’s the task here? Well, you might have noticed a flaw in our Bowl class, one that children undoubtedly love and their parents undoubtedly hate: you can put as many Scoop objects in a bowl as you like.

Let’s make the children sad, and their parents happy, by capping the number of scoops in a bowl at three. That is, you can add as many scoops in each call to Bowl.add_scoops as you want, and you can call that method as many times as you want--but only the first three scoops will actually be added. Any additional scoops will be ignored.

Working it out

We only need to make two changes to our original Bowl class for this to work.

First, we need to define a class attribute on Bowl. We do this most easily by making an assignment within the class definition (figure 9.11). Setting max_scoops = 3 within the class block is the same as saying, afterwards, Bowl.max_scoops = 3.

Figure 9.11 max_scoops sits on the class, so even an empty instance has access to it.

But wait, do we really need to define max_scoops on the Bowl class? Technically, we have two other options:

  • Define the maximum on the instance, rather than the class. This will work (i.e., add self.max_scoops = 3 in __init__), but it implies that every bowl has a different maximum number of scoops. By putting the attribute on the class (figure 9.12), we indicate that every bowl will have the same maximum.

  • We could also hardcode the value 3 in our code, rather than use a symbolic name such as max_scoops. But this will reduce our flexibility, especially if and when we want to use inheritance (as we’ll see later). Moreover, if we decide to change the maximum down the line, it’s easier to do that in one place, via the attribute assignment, rather than in a number of places.

Figure 9.12 A Bowl instance containing scoops, with max_scoops defined on the class

Second, we need to change Bowl.add_scoops, adding an if statement to make the addition of new scoops conditional on the current length of self.scoops and the value of Bowl.max_scoops.

Are class attributes just static variables?

If you’re coming from the world of Java, C#, or C++, then class attributes look an awful lot like static variables. But they aren’t static variables, and you shouldn’t call them that.

Here are a few ways class attributes are different from static variables, even though their uses might be similar:

First, class attributes are just another case of attributes on a Python object. This means that we can and should reason about class attributes the same as all others, with the ICPO lookup rule. You can access them on the class (as ClassName.attrname) or on an instance (as one_instance.attrname). The former will work because you’re using the class, and the latter will work because after checking the instance, Python checks its class.

In the solution for this exercise, Bowl.max_scoops is an attribute on the Bowl class. We could, in theory, assign max_scoops to each individual instance of Bowl, but it makes more sense to say that all Bowl objects have the same maximum number of scoops.

Second, static variables are shared among the instances and class. This means that assigning to a class variable via an instance has the same effect as assigning to it via the class. In Python, there’s a world of difference between assigning to the class variable via the instance and doing so via the class; the former will add a new attribute to the instance, effectively blocking access to the class attribute.

That is, if we assign to Bowl.max_scoops, then we’re changing the maximum number of scoops that all bowls can have. But if we assign to one_bowl.max_scoops, we’re setting a new attribute on the instance one_bowl. This will put us in the terrible situation of having Bowl.max_scoops set to one thing, and one_bowl.max_scoops set to something else. Moreover, asking for one_bowl.max_scoops would (by the ICPO rule) stop after finding the attribute on the instance and never look on the class.

Third, methods are actually class attributes too. But we don’t think of them in that way because they’re defined differently. Whatever we may think, methods are created using def inside of a class definition.

When I invoke b.add_scoops, Python looks on b for the attribute add_scoops and doesn’t find it. It then looks on Bowl (i.e., b’s class) and finds it--and retrieves the method object. The parentheses then execute the method. This only works if the method is actually defined on the class, which it is. Methods are almost always defined on a class, and thanks to the ICPO rule, Python will look for them there.

Finally, Python doesn’t have constants, but we can simulate them with class attributes. Much as I did with max_scoops earlier, I often define a class attribute that I can then access, by name, via both the class and the instances.

For example, the class attribute max_scoops is being used here as a sort of constant. Instead of storing the hardcoded number 3 everywhere I need to refer to the maximum scoops that can be put in a bowl, I can refer to Bowl.max_scoops. This both adds clarity to my code and allows me to change the value in the future in a single place.

Solution

class Scoop():
    def __init__(self, flavor):
        self.flavor = flavor
 
class Bowl():
    max_scoops = 3                                      

    def __init__(self):
        self.scoops = []
 
    def add_scoops(self, *new_scoops):
        for one_scoop in new_scoops:
            if len(self.scoops) < Bowl.max_scoops:    
                self.scoops.append(one_scoop)
 
    def __repr__(self):
        return '
'.join(s.flavor for s in self.scoops)
 
 
s1 = Scoop('chocolate')
s2 = Scoop('vanilla')
s3 = Scoop('persimmon')
s4 = Scoop('flavor 4')
s5 = Scoop('flavor 5')
 
b = Bowl()
b.add_scoops(s1, s2)
b.add_scoops(s3)
b.add_scoops(s4, s5)
print(b)

max_scoops is not a variable--it’s an attribute of the class Bowl.

Uses Bowl.max_scoops to get the maximum per bowl, set on the class

You can work through a version of this code in the Python Tutor at http://mng.bz/ NK6N.

Screencast solution

Watch this short video walkthrough of the solution: https://livebook.manning.com/ video/python-workout.

Beyond the exercise

As I’ve indicated, you can use class attributes in a variety of ways. Here are a few additional challenges that can help you to appreciate and understand how to define and use class attributes:

  • Define a Person class, and a population class attribute that increases each time you create a new instance of Person. Double-check that after you’ve created five instances, named p1 through p5, Person.population and p1.population are both equal to 5.

  • Python provides a __del__ method that’s executed when an object is garbage collected. (In my experience, deleting a variable or assigning it to another object triggers the calling of __del__ pretty quickly.) Modify your Person class such that when a Person instance is deleted, the population count decrements by 1. If you aren’t sure what garbage collection is, or how it works in Python, take a look at this article: http://mng.bz/nP2a.

  • Define a Transaction class, in which each instance represents either a deposit or a withdrawal from a bank account. When creating a new instance of Transaction, you’ll need to specify an amount--positive for a deposit and negative for a withdrawal. Use a class attribute to keep track of the current balance, which should be equal to the sum of the amounts in all instances created to date.

Inheritance in Python

The time has come for us to use inheritance, an important idea in object-oriented programming. The basic idea reflects the fact that we often want to create classes that are quite similar to one another. We can thus create a parent class, in which we define the general behavior. And then we can create one or more child classes, or subclasses, each of which inherits from the parent class:

  • If I already have a Person class, then I might want to create an Employee class, which is identical to Person except that each employee has an ID number, department, and salary.

  • If I already have a Vehicle class, then I can create a Car class, a Truck class, and a Bicycle class.

  • If I already have a Book class, then I can create a Textbook class, as well as a Novel class.

As you can see, the idea of a subclass is that it does everything the parent class does, but then goes a bit further with more specific functionality. Inheritance allows us to apply the DRY principle to our classes, and to keep them organized in our heads.

How does inheritance work in Python? Define a second class (i.e., a subclass), naming the parent class in parentheses on the first line:

class Person():
    def __init__(self, name):
        self.name = name
 
    def greet(self):
        return f'Hello, {self.name}'
 
class Employee(Person)                     
    def __init__(self, name, id_number):
        self.name = name                   
        self.id_number = id_number

This is how we tell Python that “Employee” is-a “Person,” meaning it inherits from “Person.”

Does this look funny to you? It should--more soon.

With this code in place, we can now create an instance of Employee, as per usual:

e = Employee('empname', 1)

But what happens if we invoke e.greet? By the ICPO rule, Python first looks for the attribute greet on the instance e, but it doesn’t find it. It then looks on the class Employee and doesn’t find it. Python then looks on the parent class, Person, finds it, retrieves the method, and invokes it. In other words, inheritance is a powerful idea--but in Python, it’s a natural outgrowth of the ICPO rule.

There’s one weird thing about my implementation of Employee, namely that I set self.name in ___init__. If you’re coming from a language like Java, you might be wondering why I have to set it at all, since Person.__init__ already sets it. But that’s just the thing: in Python, __init__ really needs to execute for it to set the attribute. If we were to remove the setting of self.name from Employee.__init__, the attribute would never be set. By the ICPO rule, only one method would ever be called, and it would be the one that’s closest to the instance. Since Employee.__init__ is closer to the instance than Person.__init__, the latter is never called.

The good news is that the code I provided works. But the bad news is that it violates the DRY rule that I’ve mentioned so often.

The solution is to take advantage of inheritance via super. The super built-in allows us to invoke a method on a parent object without explicitly naming that parent. In our code, we could thus rewrite Employee.__init__ as follows:

class Employee(Person)
    def __init__(self, name, id_number):
        super().__init__(name)           
        self.id_number = id_number

Implicitly invoking Person.__init__ via super

Exercise 41 A bigger bowl

While the previous exercise might have delighted parents and upset children, our job as ice cream vendors is to excite the children, as well as take their parents’ money. Our company has thus started to offer a BigBowl product, which can take up to five scoops.

Implement BigBowl for this exercise, such that the only difference between it and the Bowl class we created earlier is that it can have five scoops, rather than three. And yes, this means that you should use inheritance to achieve this goal.

You can modify Scoop and Bowl if you must, but such changes should be minimal and justifiable.

Note As a general rule, the point of inheritance is to add or modify functionality in an existing class without modifying the parent. Purists might thus dislike these instructions, which allow for changes in the parent class. However, the real world isn’t always squeaky clean, and if the classes are both written by the same team, it’s possible that the child’s author can negotiate changes in the parent class.

Working it out

This is, I must admit, a tricky one. It forces you to understand how attributes work, and especially how they interact between instances, classes, and parent classes. If you really get the ICPO rule, then the solution should make sense.

In our previous version of Bowl.add_scoops, we said that we wanted to use Bowl.max _scoops to keep track of the maximum number of scoops allowed. That was fine, as long as every subclass would want to use the same value.

But here, we want to use a different value. That is, when invoking add_scoops on a Bowl object, the maximum should be Bowl.max_scoops. And when invoking add_scoops on a BigBowl object, the maximum should be BigBowl.max_scoops. And we want to avoid writing add_scoops twice.

The simplest solution is to change our reference in add_scoops from Bowl.max _scoops, to self.max_scoops. With this change in place, things will work like this:

  • If we ask for Bowl.max_scoops, we’ll get 3.

  • If we ask for BigBowl.max_scoops, we’ll get 5.

  • If we invoke add_scoops on an instance of Bowl, then inside the method, we’ll ask for self.max_scoops. By the ICPO lookup rule, Python will look first on the instance and then on the class, which is Bowl in this case, and return Bowl.max_scoops, with a value of 3.

  • If we invoke add_scoops on an instance of BigBowl, then inside the method we’ll ask for self.max_scoops. By the iCPO lookup rule, Python will first look on the instance, and then on the class, which is BigBowl in this case, and return BigBowl.max_scoops, with a value of 5.

In this way, we’ve taken advantage of inheritance and the flexibility of self to use the same interface for a variety of classes. Moreover, we were able to implement BigBowl with a minimum of code, using what we’d already written for Bowl.

Solution

class Scoop():
    def __init__(self, flavor):
        self.flavor = flavor
 
 
class Bowl():
    max_scoops = 3                                    
 
    def __init__(self):
        self.scoops = []
 
    def add_scoops(self, *new_scoops):
        for one_scoop in new_scoops:
            if len(self.scoops) < self.max_scoops:    
                self.scoops.append(one_scoop)
 
    def __repr__(self):
        return '
'.join(s.flavor for s in self.scoops)
 
 
class BigBowl(Bowl):
    max_scoops = 5                                    

s1 = Scoop('chocolate')
s2 = Scoop('vanilla')
s3 = Scoop('persimmon')
s4 = Scoop('flavor 4')
s5 = Scoop('flavor 5')
 
bb = BigBowl()
bb.add_scoops(s1, s2)
bb.add_scoops(s3)
bb.add_scoops(s4, s5)
print(bb)

Bowl.max_scoops remains 3.

Uses self.max_scoops, rather than Bowl.max_scoops, to get the attribute from the correct class

BigBowl.max_scoops is set to 5.

You can work through a version of this code in the Python Tutor at http://mng.bz/ D2gn.

Screencast solution

Watch this short video walkthrough of the solution: https://livebook.manning.com/ video/python-workout.

Beyond the exercise

As I’ve already indicated in this chapter, I think that many people exaggerate the degree to which they should use inheritance in object-oriented code. But that doesn’t mean I see inheritance as unnecessary or even worthless. Used correctly, it’s a powerful tool that can reduce code size and improve its maintenance. Here are some more ways you can practice using inheritance:

  • Write an Envelope class, with two attributes, weight (a float, measuring grams) and was_sent (a Boolean, defaulting to False). There should be three methods: (1) send, which sends the letter, and changes was_sent to True, but only after the envelope has enough postage; (2) add_postage, which adds postage equal to its argument; and (3) postage_needed, which indicates how much postage the envelope needs total. The postage needed will be the weight of the envelope times 10. Now write a BigEnvelope class that works just like Envelope except that the postage is 15 times the weight, rather than 10.

  • Create a Phone class that represents a mobile phone. (Are there still nonmobile phones?) The phone should implement a dial method that dials a phone number (or simulates doing so). Implement a SmartPhone subclass that uses the Phone.dial method but implements its own run_app method. Now implement an iPhone subclass that implements not only a run_app method, but also its own dial method, which invokes the parent’s dial method but whose output is all in lowercase as a sign of its coolness.

  • Define a Bread class representing a loaf of bread. We should be able to invoke a get_nutrition method on the object, passing an integer representing the number of slices we want to eat. In return, we’ll receive a dict whose key-value pairs will represent calories, carbohydrates, sodium, sugar, and fat, indicating the nutritional statistics for that number of slices. Now implement two new classes that inherit from Bread, namely WholeWheatBread and RyeBread. Each class should implement the same get_nutrition method, but with different nutritional information where appropriate.

Exercise 42 FlexibleDict

I’ve already said that the main point of inheritance is to take advantage of existing functionality. There are several ways to do this and reasons for doing this, and one of them is to create new behavior that’s similar to, but distinct from, an existing class. For example, Python comes not just with dict, but also with Counter and defaultdict. By inheriting from dict, those two classes can implement just those methods that differ from dict, relying on the original class for the majority of the functionality.

In this exercise, we’ll also implement a subclass of dict, which I call FlexibleDict. Dict keys are Python objects, and as such are identified with a type. So if you use key 1 (an integer) to store a value, then you can’t use key '1' (a string) to retrieve that value. But FlexibleDict will allow for this. If it doesn’t find the user’s key, it will try to convert the key to both str and int before giving up; for example

fd = FlexibleDict()
 
fd['a'] = 100
print(fd['a'])    
 
fd[5] = 500
print(fd[5])      
 
fd[1] = 100       
print(fd['1'])    
 
fd['1'] = 100     
print(fd[1])      

Prints 100, just like a regular dict

Prints 500, just like a regular dict

int key

Prints 100, even though we passed a str

str key

Prints 100, even though we passed an int

Working it out

This exercise’s class, FlexibleDict, is an example of where you might just want to inherit from a built-in type. It’s somewhat rare, but as you can see here, it allows us to create an alternative type of dict.

The specification of FlexibleDict indicates that everything should work just like a regular dict, except for retrievals. We thus only need to override one method, the __getitem__ method that’s always associated with square brackets in Python. Indeed, if you’ve ever wondered why strings, lists, tuples, and dicts are defined in different ways but all use square brackets, this method is the reason.

Because everything should be the same as dict except for this single method, we can inherit from dict, write one method, and be done.

This method receives a key argument. If the key isn’t in the dict, then we try to turn it into a string and an integer. Because we might encounter a ValueError trying to turn a key into an integer, we trap for ValueError along the way. At each turn, we check to see if a version of the key with a different type might actually work--and, if so, we reassign the value of key.

At the end of the method, we call our parent __getitem__ method. Why don’t we just use square brackets? Because that will lead to an infinite loop, seeing as square brackets are defined to invoke __getitem__. In other words, a[b] is turned into a.__getitem__(b). If we then include self[b] inside the definition of __getitem__, we’ll end up having the method call itself. We thus need to explicitly call the parent’s method, which in any event will return the associated value.

Note While FlexibleDict (and some of the “Beyond the exercise” tasks) might be great for teaching you Python skills, building this kind of flexibility into Python is very un-Pythonic and not recommended. One of the key ideas in Python is that code should be unambiguous, and in Python it’s also better to get an error than for the language to guess.

Solution

class FlexibleDict(dict):
    def __getitem__(self, key):               
        try:
            if key in self:                   
                pass
            elif str(key) in self:            
                key = str(key)
            elif int(key) in self:            
                key = int(key)
        except ValueError:                    
            pass
 
        return dict.__getitem__(self, key)    
 
 
fd = FlexibleDict()
 
fd['a'] = 100
print(fd['a'])
 
fd[5] = 500
print(fd[5])
 
fd[1] = 100
print(fd['1'])
 
fd['1'] = 100
print(fd[1])

__getitem__ is what square brackets [] invoke.

Do we have the requested key?

If not, then tries turning it into a string

If not, then tries turning it into an integer

If we can’t turn it into an integer, then ignores it

Tries with the regular dict __getitem__, either with the original key or a modified one

You can work through a version of this code in the Python Tutor at http://mng.bz/ lGx6.

Screencast solution

Watch this short video walkthrough of the solution: https://livebook.manning.com/ video/python-workout.

Beyond the exercise

We’ve now seen how to extend a built-in class using inheritance. Here are some more exercises you can try, in which you’ll also experiment with extending some built-in classes:

  • With FlexibleDict, we allowed the user to use any key, but were then flexible with the retrieval. Implement StringKeyDict, which converts its keys into strings as part of the assignment. Thus, immediately after saying skd[1] = 10, you would be able to then say skd['1'] and get the value of 10 returned. This can come in handy if you’ll be reading keys from a file and won’t be able to distinguish between strings and integers.

  • The RecentDict class works just like a dict, except that it contains a user-defined number of key-value pairs, which are determined when the instance is created. In a RecentDict(5), only the five most recent key-value pairs are kept; if there are more than five pairs, then the oldest key is removed, along with its value. Note: your implementation could take into account the fact that modern dicts store their key-value pairs in chronological order.

  • The FlatList class inherits from list and overrides the append method. If append is passed an iterable, then it should add each element of the iterable separately. This means that fl.append([10, 20, 30]) would not add the list [10, 20, 30] to fl, but would rather add the individual integers 10, 20, and 30. You might want to use the built-in iter function (http://mng.bz/Qy2G) to determine whether the passed argument is indeed iterable.

Exercise 43 Animals

For the final three exercises in this chapter, we’re going to create a set of classes that combine all of the ideas we’ve explored in this chapter: classes, methods, attributes, composition, and inheritance. It’s one thing to learn about and use them separately, but when you combine these techniques together, you see their power and understand the organizational and semantic advantages that they offer.

For the purposes of these exercises, you are the director of IT at a zoo. The zoo contains several different kinds of animals, and for budget reasons, some of those animals have to be housed alongside other animals.

We will represent the animals as Python objects, with each species defined as a distinct class. All objects of a particular class will have the same species and number of legs, but the color will vary from one instance to another. We can thus create a white sheep:

s = Sheep('white')

I can similarly get information about the animal back from the object by retrieving its attributes:

print(s.species)          
print(s.color)            
print(s.number_of_legs)   

Prints “sheep”

Prints “white”

Prints “4”

If I convert the animal to a string (using str or print), I’ll get back a string combining all of these details:

print(s)    

Prints “White sheep, 4 legs”

We’re going to assume that our zoo contains four different types of animals: sheep, wolves, snakes, and parrots. (The zoo is going through some budgetary difficulties, so our animal collection is both small and unusual.) Create classes for each of these types, such that we can print each of them and get a report on their color, species, and number of legs.

Working it out

The end goal here is somewhat obvious: we want to have four different classes (Wolf, Sheep, Snake, and Parrot), each of which takes a single argument representing a color. The result of invoking each of these classes is a new instance with three attributes: species, color, and number_of_legs.

A naive implementation would simply create each of these four classes. But of course, part of the point here is to use inheritance, and the fact that the behavior in each class is basically identical means that we can indeed take advantage of it. But what will go into the Animal class, from which everyone inherits, and what will go into each of the individual subclasses?

Since all of the animal classes will have the same attributes, we can define __repr__ on Animal, the class from which they’ll all inherit. My version uses an f-string and grabs the attributes from self. Note that self in this case will be an instance not of Animal, but of one of the classes that inherits from Animal.

So, what else should be in Animal, and what should be in the subclasses? There’s no hard-and-fast rule here, but in this particular case, I decided that Animal.__init__ would be where the assignments all happen, and that the __init__ method in each subclass would invoke Animal.__init__ with a hardcoded number of legs, as well as the color designated by the user (figure 9.13).

Figure 9.13 Wolf inherits from Animal. Notice which methods are defined where.

In theory, __init__ in a subclass could call Animal.__init__ directly and by name. But we also have access to super, which returns the object on which our method should be called. In other words, by calling super().__init__, we know that the right method will be called on the right object, and can just pass along the color and number_of_legs arguments.

But wait, what about the species attribute? How can we set that without input from the user?

My solution to this problem was to take advantage of the fact that Python classes are very similar to modules, with similar behavior. Just as a module has a __name__ attribute that reflects what module was loaded, so too classes have a __name__ attribute, which is a string containing the name of the current class. And thus, if I invoke self.__class__ on an object, I get its class--and if I invoke self.__class__.__name__, I get a string representation of the class.

Abstract base classes

The Animal class here is what other languages might call an abstract base class, namely one that we won’t actually instantiate, but from which other classes will inherit. In Python, you don’t have to declare such a class to be abstract, but you also won’t get the enforcement that other languages provide. If you really want, you can import ABCMeta from the abc (abstract base class) module. Following its instructions, you’ll be able to declare particular methods as abstract, meaning that they must be overridden in the child.

I’m not a big fan of abstract base classes; I think that it’s enough to document a class as being abstract, without the overhead or language enforcement. Whether that’s a smart approach depends on several factors, including the nature and size of the project you’re working on and whether you come from a background in dynamic languages. A large project, with many developers, would probably benefit from the additional safeguards that an abstract base class would provide.

If you want to learn more about abstract base classes in Python, you can read about ABCMeta here: http://mng.bz/yyJB.

Solution

class Animal():
    def __init__(self, color, number_of_legs):     
        self.species = self.__class__.__name__     
        self.color = color
        self.number_of_legs = number_of_legs

    def __repr__(self):
        return f'{self.color} {self.species},
        {self.number_of_legs} legs'              
 

class Wolf(Animal):
    def __init__(self, color):
        super().__init__(color, 4)

class Sheep(Animal):
    def __init__(self, color):
        super().__init__(color, 4)

class Snake(Animal):
    def __init__(self, color):
        super().__init__(color, 0)

class Parrot(Animal):
    def __init__(self, color):
        super().__init__(color, 2)
 

wolf = Wolf('black')
sheep = Sheep('white')
snake = Snake('brown')
parrot = Parrot('green')

print(wolf)
print(sheep)
print(snake)
print(parrot)

Our Animal base class takes a color and number of legs.

Turns the current class object into a string

Uses an f-string to produce appropriate output

You can work through a version of this code in the Python Tutor at http://mng.bz/ B2Z0.

Screencast solution

Watch this short video walkthrough of the solution: https://livebook.manning.com/ video/python-workout.

Beyond the exercise

In this exercise, we put a few classes in place as part of a hierarchy. Here are some additional ways you can work with inheritance and think about the implications of the design decisions we’re making. I should note that these questions, as well as those following in this chapter, are going to combine hands-on practice with some deeper, philosophical questions about the “right” way to work with object-oriented systems:

  • Instead of each animal class inheriting directly, from Animal, define several new classes, ZeroLeggedAnimal, TwoLeggedAnimal, and FourLeggedAnimal, all of which inherit from Animal, and dictate the number of legs on each instance. Now modify Wolf, Sheep, Snake, and Parrot such that each class inherits from one of these new classes, rather than directly from Animal. How does this affect your method definitions?

  • Instead of writing an __init__ method in each subclass, we could also have a class attribute, number_of_legs, in each subclass--similar to what we did earlier with Bowl and BigBowl. Implement the hierarchy that way. Do you even need an __init__ method in each subclass, or will Animal.__init__ suffice?

  • Let’s say that each class’s __repr__ method should print the animal’s sound, as well as the standard string we implemented previously. In other words, str(sheep) would be Baa--white sheep, 4 legs. How would you use inheritance to maximize code reuse?

Exercise 44 Cages

Now that we’ve created some animals, it’s time to put them into cages. For this exercise, create a Cage class, into which you can put one or more animals, as follows:

c1 = Cage(1)
c1.add_animals(wolf, sheep)
 
c2 = Cage(2)
c2.add_animals(snake, parrot)

When you create a new Cage, you’ll give it a unique ID number. (The uniqueness doesn’t need to be enforced, but it’ll help us to distinguish among the cages.) You’ll then be able to invoke add_animals on the new cage, passing any number of animals that will be put in the cage. I also want you to define a __repr__ method so that printing a cage prints not just the cage ID, but also each of the animals it contains.

Working it out

The solution’s definition of the Cage class is similar in some ways to the Bowl class that we defined earlier in this chapter.

When we create a new cage, the __init__ method initializes self.animals with an empty list, allowing us to add (and even remove) animals as necessary. We also store the ID number that was passed to us in the id_number parameter.

Next, we implement Cage.add_animals, which uses similar techniques to what we did in Bowl.add_scoops. Once again, we use the splat (*) operator to grab all arguments in a single tuple (animals). Although we could use list.extend to add all of the new animals to list.animals, I’ll still use a for loop here to add them one at a time. You can see how the Python Tutor depicts two animals in a cage in figure 9.14.

The most interesting part of our Cage definition, in my mind, is our use of __repr__ to produce a report. Given a cage c1, saying print(c1) will print the ID of the cage, followed by all of the animals in the cage, using their printed representations. We do this by first printing a basic header, which isn’t a huge deal. But then we take each animal in self.animals and use a generator expression (i.e., a lazy form of list comprehension) to return a sequence of strings. Each string in that sequence will consist of a tab followed by the printed representation of the animal. We then feed the result of our generator expression to str.join, which puts newline characters between each animal.

Figure 9.14 A Cage instance containing one wolf and one sheep

Solution

class Animal():
    def __init__(self, color, number_of_legs):
        self.species = self.__class__.__name__
        self.color = color
        self.number_of_legs = number_of_legs

    def __repr__(self):
        return f'{self.color} {self.species}, {self.number_of_legs} legs'

class Wolf(Animal):
    def __init__(self, color):
        super().__init__(color, 4)
 

class Sheep(Animal):
    def __init__(self, color):
        super().__init__(color, 4)
 

class Snake(Animal):
    def __init__(self, color):
        super().__init__(color, 0)
 

class Parrot(Animal):
    def __init__(self, color):
        super().__init__(color, 2)
 

class Cage():
    def __init__(self, id_number):
        self.id_number = id_number            
        self.animals = []                     
 
    def add_animals(self, *animals):
        for one_animal in animals:
            self.animals.append(one_animal)
 
    def __repr__(self):                       
        output = f'Cage {self.id_number}
'
        output += '
'.join('	' + str(animal)
                            for animal in self.animals)
        return output
 
wolf = Wolf('black')
sheep = Sheep('white')
snake = Snake('brown')
parrot = Parrot('green')
 
c1 = Cage(1)
c1.add_animals(wolf, sheep)
 
c2 = Cage(2)
c2.add_animals(snake, parrot)
 
print(c1)
print(c2)

Sets an ID number for each cage, just so that we can distinguish their printouts

Sets up an empty list, into which we’ll place animals

The string for each cage will mainly be from a string, based on a generator expression.

You can work through a version of this code in the Python Tutor at http://mng.bz/ dyeN.

Screencast solution

Watch this short video walkthrough of the solution: https://livebook.manning.com/ video/python-workout.

Beyond the exercise

We’re once again seeing the need for composition in our classes--creating objects that are containers for other objects. Here are some possible extensions to this code, all of which draw on the ideas we’ve already seen in this chapter, and which you’ll see repeated in nearly every object-oriented system you build and encounter:

  • As you can see, there are no limits on how many animals can potentially be put into a cage. Just as we put a limit of three scoops in a Bowl and five in a BigBowl, you should similarly create Cage and BigCage classes that limit the number of animals that can be placed there.

  • It’s not very realistic to say that we would limit the number of animals in a cage. Rather, it makes more sense to describe how much space each animal needs and to ensure that the total amount of space needed per animal isn’t greater than the space in each cage. You should thus modify each of the Animal subclasses to include a space_required attribute. Then modify the Cage and BigCage classes to reflect how much space each one has. Adding more animals than the cage can contain should raise an exception.

  • Our zookeepers have a macabre sense of humor when it comes to placing animals together, in that they put wolves and sheep in the first cage, and snakes and birds in the other cage. (The good news is that with such a configuration, the zoo will be able to save on food for half of the animals.) Define a dict describing which animals can be with others. The keys in the dict will be classes, and the values will be lists of classes that can compatibly be housed with the keys. Then, when adding new animals to the current cage, you’ll check for compatibility. Trying to add an animal to a cage that already contains an incompatible animal will raise an exception.

Exercise 45 Zoo

Finally, the time has come to create our Zoo object. It will contain cage objects, and they in turn will contain animals. Our Zoo class will need to support the following operations:

  • Given a zoo z, we should be able to print all of the cages (with their ID numbers) and the animals inside simply by invoking print(z).

  • We should be able to get the animals with a particular color by invoking the method z.animals_by_color. For example, we can get all of the black animals by invoking z.animals_by_color('black'). The result should be a list of Animal objects.

  • We should be able to get the animals with a particular number of legs by invoking the method z.animals_by_legs. For example, we can get all of the four-legged animals by invoking z.animals_by_legs(4). The result should be a list of Animal objects.

  • Finally, we have a potential donor to our zoo who wants to provide socks for all of the animals. Thus, we need to be able to invoke z.number_of_legs() and get a count of the total number of legs for all animals in our zoo.

The exercise is thus to create a Zoo class on which we can invoke the following:

z = Zoo()
z.add_cages(c1, c2)
 
print(z)
print(z.animals_by_color('white'))
print(z.animals_by_legs(4))
print(z.number_of_legs())

Working it out

In some ways, our Zoo class here is quite similar to our Cage class. It has a list attribute, self.cages, in which we’ll store the cages. It has an add_cages method, which takes *args and thus takes any number of inputs. Even the __repr__ method is similar to what we did with Cage.__repr__. We’ll simply use str.join on the output from running str on each of the cages, just as the cages run str on each of the animals. We’ll similarly use a generator expression here, which will be slightly more efficient than a list comprehension.

But then, when it comes to the three methods we needed to create, we’ll switch direction a little bit. In both animals_by_color and animals_by_legs, we want to get the animals with a certain color or a certain number of legs. Here, we take advantage of the fact that the zoo contains a list of cages, and that each cage contains a list of animals. We can thus use a nested list comprehension, getting a list of all of the animals.

But of course, we don’t want all of the animals, so we have an if statement that filters out those that we don’t want. In the case of animals_by_color, we only include those animals that have the right color, and in animals_by_legs, we only keep those animals with the requested number of legs.

But then we also have number_of_legs, which works a bit differently. There, we want to get an integer back, reflecting the number of legs that are in the entire zoo. Here, we can take advantage of the built-in sum method, handing it the generator expression that goes through each cage and retrieves the number of legs on each animal. The method will thus return an integer.

Although the object-oriented and functional programming camps have been fighting for decades over which approach is superior, I think that the methods in this Zoo class show us that each has its strengths, and that our code can be short, elegant, and to the point if we combine the techniques. That said, I often get pushback from students who see this code and say that it’s a violation of the object-oriented principle of encapsulation, which ensures that we can’t (or shouldn’t) directly access the data in other objects.

Whether this is right or wrong, such violations are also fairly common in the Python world. Because all data is public (i.e., there’s no private or protected), it’s considered a good and reasonable thing to just scoop the data out of objects. That said, this also means that whoever writes a class has a responsibility to document it, and to keep the API alive--or to document elements that may be deprecated or removed in the future.

Solution

This is the longest and most complex class definition in this chapter--and yet, each of the methods uses techniques that we’ve discussed, both in this chapter and in this book:

class Zoo():
    def __init__(self):
        self.cages = []                               

    def add_cages(self, *cages):
        for one_cage in cages:
            self.cages.append(one_cage)
 
    def __repr__(self):
        return '
'.join(str(one_cage)
                         for one_cage in self.cages)
 
    def animals_by_color(self, color):                
        return [one_animal
                for one_cage in self.cages
                for one_animal in one_cage.animals
                if one_animal.color == color]
 
    def animals_by_legs(self, number_of_legs):        
        return [one_animal
                for one_cage in self.cages
                for one_animal in one_cage.animals
                if one_animal.number_of_legs ==
                        number_of_legs]
 
    def number_of_legs(self):                         
        return sum(one_animal.number_of_legs
                   for one_cage in self.cages
                   for one_animal in one_cage.animals)
 
wolf = Wolf('black')
sheep = Sheep('white')
snake = Snake('brown')
parrot = Parrot('green')
 
print(wolf)
print(sheep)
print(snake)
print(parrot)
 
c1 = Cage(1)
c1.add_animals(wolf, sheep)
 
c2 = Cage(2)
c2.add_animals(snake, parrot)
 
z = Zoo()
z.add_cages(c1, c2)
 
print(z)
print(z.animals_by_color('white'))
print(z.animals_by_legs(4))
print(z.number_of_legs())

Sets up the self.cages attribute, a list where we’ll store cages

Defines the method that’ll return animal objects that match our color

Defines the method that’ll return animal objects that match our number of legs

Returns the number of legs

You can work through a version of this code in the Python Tutor at http://mng.bz/ lGMB.

Screencast solution

Watch this short video walkthrough of the solution: https://livebook.manning.com/ video/python-workout.

Beyond the exercise

Now that you’ve seen how all of these elements fit together in our Zoo class, here are some additional exercises you might want to try out, to extend what we’ve done--and to better understand object-oriented programming in Python:

  • Modify animals_by_color such that it takes any number of colors. Animals having any of the listed colors should be returned. The method should raise an exception if no colors are passed.

  • As things currently stand, we’re treating our Zoo class almost as if it’s a singleton object--that is, a class that has only one instance. What a sad world that would be, with only one zoo! Let’s assume, then, that we have two instances of Zoo, representing two different zoos, and that we would like to transfer an animal from one to the other. Implement a Zoo.transfer_animal method that takes a target_zoo and a subclass of Animal as arguments. The first animal of the specified type is removed from the zoo on which we’ve called the method and inserted into the first cage in the target zoo.

  • Combine the animals_by_color and animals_by_legs methods into a single get_animals method, which uses kwargs to get names and values. The only valid names would be color and legs. The method would then use one or both of these keywords to assemble a query that returns those animals that match the passed criteria.

Summary

Object-oriented programming is a set of techniques, but it’s also a mindset. In many languages, object-oriented programming is forced on you, such that you’re constantly trying to fit your programming into its syntax and structure. Python tries to strike a balance, offering all of the object-oriented features we’re likely to want or use, but in a simple, nonconfrontational way. In this way, Python’s objects provide us with structure and organization that can make our code easier to write, read, and (most importantly) maintain.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset