Object-Oriented Programming

Learning Objectives

By the end of this chapter, you will be able to:

  • Explain different OOP concepts and the importance of OOP
  • Instantiate a class
  • Describe how to define instance methods and pass arguments to them
  • Declare class attributes and class methods
  • Describe how to override methods
  • Implement multiple inheritance

This lesson introduces object-oriented programming as implemented in Python. We also cover classes and methods, as well as overriding methods and inheritance.

Introduction

A programming paradigm is a style of reasoning about programming problems. Problems, in general, can often be solved in multiple ways; for example, to calculate the sum of 2 and 3, you can use a calculator, you can use your fingers, you can use a tally mark, and so on. Similarly, in programming, you can solve problems in different ways.

At the beginning of this book, we mentioned that Python is multi-paradigm, as it supports solving problems in a functional, imperative, procedural, and object-oriented way. In this chapter, we will be diving into object-oriented programming in Python.

A First Look at OOP

Object-oriented Programming (OOP) is a programming paradigm based on the concept of objects. Objects can be thought of as capsules of properties and procedures/methods. In an interview with Rolling Stone magazine, Steve Jobs, co-founder of Apple, once explained OOP in the following way:

"Objects are like people. They're living, breathing things that have knowledge inside them about how to do things and have memory inside them so that they can remember things. And rather than interacting with them at a very low level, you interact with them at a very high level of abstraction…"

Steve Jobs; Rolling Stone; June 16, 1994

An example of an object you can consider is a car. A car has multiple different attributes. It has a number of doors, a color, and a transmission type (for example, manual or automatic). A car, regardless of the type, also has specific behaviors: it can start, accelerate, decelerate, and change gears. Regardless of how these behaviors are implemented, the only thing we, the users of the car, care about, is that the aforementioned behaviors, such as acceleration, actually work.

In OOP, reasoning about data as objects allows us to abstract the actual code and think more about the attributes of the data and the operations around the data. OOP offers the following advantages:

  • It makes code reusable.
  • It makes it easier to design software as you can model it in terms of real-world objects.
  • It makes it easier to test, debug, and maintain.
  • The data is secure due to abstraction and data hiding.

With the benefits it confers, OOP is a powerful tool in a programmer's tool box.
In the next section, we'll be looking at how OOP is used in Python.

OOP in Python

Classes are a fundamental building block of object-oriented programming. They can be likened to blueprints for an object, as they define what properties and methods/behaviors an object should have.

For example, when building a house, you'd follow a blueprint that tells you things such as how many rooms the house has, where the rooms are positioned relative to one another, or how the plumbing and electrical circuitry is laid out. In OOP, this building blueprint would be the class, while the house would be the instance/object.

In the earlier lessons, we mentioned that everything in Python is an object. Every data type and data structure you've encountered thus far, from lists and strings to integers, functions, and others, are objects. This is why when we run the type function on any object, it will have the following output:

>>> type([1, 2, 3])

<class 'list'>

>>> type("foobar")

<class 'str'>

>>> type({"a": 1, "b": 2})

<class 'dict'>

>>> def func(): return True

...

>>> type(func)

<class 'function'>

>>>

You'll note that calling the type function on each object prints out that it is an instance of a specific class. Lists are instances of the list class, strings are instances of the str class, dictionaries are instances of the dict class, and so on and so forth.

Each class is a blueprint that defines what behaviors and attributes objects will contain and how they'll behave; for example, all of the lists that you create will have the lists.append() method, which allows you to add elements to the list.

Here, we are creating an instance of the list class and printing out the append and remove methods. It tells us that they are methods of the list object we've instantiated:

>>> l = [1, 2, 3, 4, 5] # create a list object

>>> print(l.append)

<built-in method append of list object at 0x10dd36a08>

>>> print(l.remove)

<built-in method remove of list object at 0x10dd36a08>

>>>

Note

In this chapter, we will use the terms instance and object synonymously.

Defining a Class in Python

In our example, we'll be creating the blueprint for a person. Compared to most languages, the syntax for defining a simple class is very minimal in Python.

Exercise 31: Creating a Class

In this exercise, we will create our first class, called Person. The steps are as follows:

  1. Declare the class using the Python keyword class, followed by the class name Person. In the block, we have the Python keyword pass, which is used as a placeholder for where the rest of our class definition will go:

    >>> class Person:

    ... pass

    ...

    >>>

  2. Run the type function on this class we've created; it will yield the type type:

    >>> type(Person)

    <class 'type'>

    >>>

This is a bit confusing, but what this means is just as there are data structures of type list or dict, we've also, in a sense, extended the Python language to include a new kind of data structure called Person. In Python, a class and a type are synonymous. This Person structure can encapsulate different attributes and methods that will be specific to that object. We'll look at this in more depth further down the line.

Instantiating an Object

Having a blueprint for building something is a great first step. However, blueprints aren't very useful if you can't build what they describe. Instantiating an object of a class is the act of building what the blueprint/class describes.

Exercise 32: Instantiating a Person Object

From the Person class we've defined, we'll instantiate a Person object. The steps are as follows:

  1. Create a Person object and assign it to the jack variable:

    >>> jack = Person()

  2. Create another object and assign it to the jill variable:

    >>> jill = Person()

  3. Make a comparison between jack and jill to check whether they are different objects:

    >>> jack is jill

    False

    You will find that they are. This is because whenever we instantiate an object, it creates a brand-new object.

  4. Assign jack2 to jack:

    >>> jack2 = jack

    >>> jack2 is jack

    True

    Assigning another variable to jack simply points it to whatever object jack is pointing to, and so they are the same object and thus identical.

Adding Attributes to an Object

An attribute is a specific characteristic of an object.

In Python, you can add attributes dynamically to an already instantiated object by writing the name of the object followed by a dot (.) and the name of the attribute you want to add, and assigning it to a value:

>>> person1 = Person()

>>> person1.name = "Gol D. Roger"

However, setting attributes in this manner is a bad practice, since it leads to hard-to-read code that's hard to debug. We'll see the appropriate way of setting attributes in the next section.

You can get the value of an attribute by using a similar syntax:

>>> person1.name

'Gol D. Roger'

>>>

Every object in Python comes with built-in attributes, such as __dict__, which is a dictionary that holds all of the attributes of the object:

>>> person1 = Person()

>>> person1.__dict__

{}

>>> person1.name = "Gol D. Roger"

>>> person1.age = 53

>>> person1.height_in_cm = 180

>>> person.__dict__

{'age': 53, 'height_in_cm': 180, 'name': 'Gol D. Roger'}

>>> print(person1.name, person1.age, person1.height_in_cm)

Gol D. Roger 53 180

>>>

The __init__ Method

The appropriate way to add attributes to an object is by defining them in the object's constructor method. A constructor method resides in the class and is called to create an object. It often takes arguments that are used in setting attributes of that instantiated object.

In Python, the constructor method for an object is named __init__(). As its name suggests, it is called when initializing an object of a class. Because of this, you can use it to pass the initial attributes you want your object to be constructed with.

The hasattr() function checks whether an object has a specific attribute or method. When we call the hasattr() function on the Person class to check whether it has an __init__ method, it returns True. This applies for all instances of the class, too:

>>> hasattr(Person, '__init__')

True

>>> person1 = Person()

>>> hasattr(person1, '__init__')

True

>>>

This method is here because it is inherited. We'll be taking a closer look at what inheritance is in later in this chapter.

We can define this constructor method in our class just like a function and specify attributes that will need to be passed in when instantiating an object:

>>> class Person:

... def __init__(self, name):

... self.name = name

...

>>>

Earlier on, we likened classes to blueprints for objects. In the preceding example, we're adding more details to that blueprint, and stating that every Person object that will be created should have a name attribute.

We have the arguments self and name in the __init__ method signature. The name argument refers to the person's name, while self refers to the object we're currently in the process of creating.

Remember, the __init__ method is called when instantiating objects of the Person class, and since every person has a different name, we need to be able to assign different values to different instances. Therefore, we attach our current object's name attribute in the line self.name = name.

Let's test this out.

Exercise 33: Adding Attributes to a Class

In this exercise, we will add attributes to our Person class. The steps are as follows:

  1. First, instantiate an object without passing any arguments:

    >>> person1 = Person()

    Traceback (most recent call last):

    File "<stdin>", line 1, in <module>

    TypeError: __init__() missing 1 required positional argument: 'name'

    >>>

    Python throws us an error since now we need to pass in a name argument when instantiating a Person object. This argument is passed to the __init__ method when the object is being instantiated.

  2. Instantiate a Person object, passing an argument for name:

    >>> person1 = Person("Bon Clay")

  3. Now try to access the attribute from our instance:

    >>> person1.name

    'Bon Clay'

    >>>

  4. Redefine the Person class so that it is defining more attributes that instances should be initialized with, for example, name, age, and height in centimeters:

    >>> class Person:

    ... def __init__(self, name, age, height_in_cm):

    ... self.name = name

    ... self.age = age

    ... self.height_in_cm = height_in_cm

    ...

    >>>

  5. Now, when instantiating a Person object, we'll need to pass in the three arguments: name, age, and height_in_cm. Pass in the three values, as shown here:

    >>> person1 = Person("Cubert", 62, 180)

    >>> print(person1.name, person1.age, person1.height_in_cm)

    Cubert 62 180

    >>>

Activity 28: Defining a Class and Objects

Suppose you are a backend developer for a tech news platform. You have been asked to design a templating system for their news articles. To do this, you will need to run some proof of concepts.

Define the MobilePhone class in a file named mobile_phone1.py so that the following code runs without error:

# Class definition goes here

pearphone = MobilePhone(5.5, "3GB", "yOS 11.2")

simsun = MobilePhone(5.4, "4GB", "Cyborg 8.1")

print(f"The new Pear phone has a {pearphone.display_size}"

f" inch display. {pearphone.ram} of RAM and runs on "

f"the latest version of {pearphone.os}. Its biggest competitor is "

f"the Simsun phone which sports a similar AMOLED {simsun.display_size} "

f"inch display, {simsun.ram} of RAM and runs {simsun.os}."

)

After defining the class, running the preceding code should yield the following output:

Figure 7.1: Output of running the mobile_phone1.py script
Figure 7.1: Output of running the mobile_phone1.py script

Note

Solution for this activity can be found at page 288.

Methods in a Class

In this topic, we will look at class methods in detail.

Defining Methods in a Class

So far, we've seen how to add attributes to an object. As we mentioned earlier, objects are also comprised of behaviors known as methods. Now we will take a look at how to add our own methods to classes.

Exercise 34: Creating a Method for our Class

We'll rewrite our original Person class to include a speak() method. The steps are as follows:

  1. Create a speak() method in our Person class, as follows:

    class Person:

    def __init__(self, name, age, height_in_cm):

    self.name = name

    self.age = age

    self.height_in_cm = height_in_cm

    def speak(self):

    print("Hello!")

    The syntax for defining an instance method is familiar. We pass the argument self which, as in the __init__ method, refers to the current object at hand. Passing self will allow us to get or set the object's attributes inside our function. It is always the first argument of an instance method.

  2. Instantiate an object and call the method we've defined:

    >>> adam = Person("Adam", 47, 193)

    >>> adam.speak()

    Hello!

    >>>

  3. Access instance attributes and use them inside our method by using self, as follows:

    class Person:

    def __init__(self, name, age, height_in_cm):

    self.name = name

    self.age = age

    self.height_in_cm = height_in_cm

    def speak(self):

    print(f"Hello! My name is {self.name}. I am {self.age} years old.")

    >>> adam = Person("adam", 47, 193)

    >>> lovelace = Person("Lovelace", 24, 178)

    >>> lucre = Person("Lucre", 13, 154)

    >>> adam.speak()

    Hello! My name is Adam. I am 47 years old.

    >>> lovelace.speak()

    Hello! My name is Lovelace. I am 24 years old.

    >>> lucre.speak()

    Hello! My name is Lucre. I am 13 years old.

    >>>

    As you can see, the output is dependent on the object we're calling the method on.

Passing Arguments to Instance Methods

Just as with normal functions, you can pass arguments to methods in a class. Let's now learn how to pass arguments to instance methods.

Exercise 35: Passing Arguments to Instance Methods

In this exercise, we'll create a new method called greet() that takes in an argument, person, which is a Person object.

  1. Define the greet() method in the Person class:

    class Person:

    def __init__(self, name, age):

    self.name = name

    self.age = age

    def speak(self):

    print(f"Hello! My name is {self.name}. I am {self.age} years old.")

    def greet(self, person):

    print(f"Hi {person.name}")

    Note

    We do not have to specify the method return type as you would in statically typed languages such as Java.

  2. Instantiate two new Person objects and call the greet() method to test this out:

    >>> joe = Person("Josef", 31)

    >>> gabby = Person("Gabriela", 32)

    >>> joe.greet(gabby)

    Hi Gabriela

    >>>

  3. Add more logic to the method that checks for the person's name, and print out a different message if the person is named Rogers:

    class Person:

    def __init__(self, name, age):

    self.name = name

    self.age = age

    def speak(self):

    print(f"Hello! My name is {self.name}. I am {self.age} years old.")

    def greet(self, person):

    if person.name == "Rogers":

    print("Hey neighbour!")

    else:

    print(f"Hi {person.name}")

  4. Test out the new implementation of the greet method:

    >>> joe = Person("Josef", 31)

    >>> john = Person("John", 5)

    >>> rogers = Person("Rogers", 46)

    >>> john.greet(rogers)

    Hey neighbour!

    >>> john.greet(joe)

    Hi Josef

Exercise 36: Setting Instance Attributes within Instance Methods

We'll create a birthday() method that increments the person's age.

  1. First, implement the birthday() method, which takes the age and increments it by 1:

    class Person:

    def __init__(self, name, age):

    self.name = name

    self.age = age

    def speak(self):

    print(f"Hello! My name is {self.name}. I am {self.age} years old.")

    def birthday(self):

    self.age += 1

  2. Create a person instance and check the age:

    >>> diana = Person("Diana", 28)

    >>> diana.age

    28

  3. Call the birthday method and check the age again:

    >>> diana.birthday()

    >>> diana.age

    29

    >>>

Congratulations, you can now define and use classes. You can add methods and attributes to them, as well as instantiate objects and use them. While there's more to learn, you have the necessary tools to build basic object-oriented programs.

Activity 29: Defining Methods in a Class

You are part of a team building a program to help children learn math. Currently, you're building a module on shapes, more specifically, calculating the circumference and area of circles.

The formula for calculating the circumference of a circle is 2*π*r. The formula for calculating the area of a circle is π*r*r.

Write a Python class named Circle, constructed by a radius and two methods, which will calculate the circumference and the area of a circle. The script should ask for the user's input for the radius, create a Circle object, and print out its area and circumference. It should ask for input again after it prints the area and circumference each time. Our aim here is to practice defining methods in a class.

The steps are as follows:

  1. Create a file named circle.py.
  2. Define the Circle class and the Circle constructor method.
  3. Create the area calculation method, which returns the circle's area.
  4. Create the circumference method, which returns the circle's circumference.
  5. After the class definition, add the code that requests for user input for the radius.
  6. We'll create a while loop so that the request for user input runs multiple times. In the while loop, we'll request the user input, change the circle object's radius, and then print out the area and circumference.
  7. Once you have saved the script, you can run it by using python circle.py. The script will ask for input, calculate the area and circumference, print that out, and ask for your input again. The output should look as follows:
Figure 7.2: Output of running the circle.py script
Figure 7.2: Output of running the circle.py script

Note

Solution for this activity can be found at page 288.

Class Versus Instance Attributes

In the previous section, we had an introduction to classes and attributes. The attributes we've seen defined up until this point are instance attributes. This means that they are bound to a specific instance. Initializing an object with specific attributes applies/binds those attributes to only that object, but not to any other object initialized from that class.

Exercise 37: Declaring a Class with Instance Attributes

In this exercise, we'll declare a WebBrowser class that has the attributes for history, the current page, and a flag that shows whether it's incognito or not. It can be initialized with a page.

Note

The attributes that we will declare inside the constructor will be added as instance attributes The binding of the attributes to the instance happens in the __init__ method, where we add attributes to self.

  1. Define the WebBrowser class as follows:

    class WebBrowser:

    def __init__(self, page):

    self.history = [page]

    self.current_page = page

    self.is_incognito = False

  2. Then, initialize the objects from the class:

    >>> firefox = WebBrowser("google.com")

    >>> chrome = WebBrowser("facebook.com")

    >>>

  3. Every WebBrowser instance will have a different current_page attribute. This happens because these attributes are bound to the instance and not to the class; they are instance attributes. Check this by getting the current_page attribute on different WebBrowser instances:

    >>> firefox.current_page

    'google.com'

    >>> chrome.current_page

    'facebook.com'

    >>>

Class Attributes

We can also define attributes at the class level. Class attributes are bound to the class itself and are shared by all instances as opposed to being bound to each instance.

Exercise 38: Extending our Class with Class Attributes

In this exercise, we'll add a class attribute to our WebBrowser class. The syntax for this is just like defining a variable. You simply define it in the class body. The steps are as follows:

  1. Add the connected attribute to our class. This is a Boolean showing whether the web browser has an active internet connection:

    class WebBrowser:

    connected = True

    def __init__(self, page):

    self.history = [page]

    self.current_page = page

    self.is_incognito = False

  2. Then, instantiate a WebBrowser object. We can see that the connected attribute is True for all instances:

    >>> firefox = WebBrowser("google.com")

    >>> iceweasel = WebBrowser("facebook.com")

    >>> firefox.connected

    True

    >>> iceweasel.connected

    True

    >>>

  3. Since a class attribute is bound to the class and not the instance, we can access class attributes via the class itself. Do this as follows:

    >>> WebBrowser.connected

    True

    >>>

  4. Print out our instances' __dict__ attributes; we'll see that they do not have the connected attribute:

    >>> iceweasel.__dict__

    {'history': ['facebook.com'], 'current_page': 'facebook.com', 'is_incognito': False}

    >>> firefox.__dict__

    {'history': ['google.com'], 'current_page': 'google.com', 'is_incognito': False}

    >>>

  5. Why, then, don't we get an AttributeError when we try to retrieve this attribute? This is because when we access a class attribute from an instance, it retrieves it from the class itself. Here, we can see that the WebBrowser class's __dict__ contains the connected attribute:

    >>> WebBrowser.__dict__

    mappingproxy({'__module__': '__main__', 'connected': True, '__init__': <function WebBrowser.__init__ at 0x10cc6ad08>, '__dict__': <attribute '__dict__' of 'WebBrowser' objects>, '__weakref__': <attribute '__weakref__' of 'WebBrowser' objects>, '__doc__': None})

    >>>

    Note

    Since instances retrieve the attribute from the class, when we change this class attribute through the class, it'll reflect on all existing instances.

  6. Therefore, we need to be cautious when changing a class attribute through an instance because doing so will create an instance attribute and will no longer retrieve the attribute from the class. Check this, as follows:

    >>> firefox.connected = False

    >>>

  7. Print out the __dict__ attribute of the object; we'll see that it now has a new instance attribute, connected:

    >>> firefox.__dict__

    {'history': ['google.com'], 'current_page': 'google.com', 'is_incognito': False, 'connected': False}

    >>>

  8. This means that when we try to get the connected attribute, it will no longer try retrieving it from the class, but will instead retrieve the attribute bound to the object. Despite this change we've made, the WebBrowser class attribute remains the same. Check this, as follows:

    >>> WebBrowser.connected

    True

    >>>

Exercise 39: Implementing a Counter for Instances of a Class

Our aim here is to thoroughly understand class attributes. In this exercise, we're going to create a counter that will be incremented each time a new WebBrowser object is instantiated:

  1. Add the class attribute number_of_web_browsers, which will serve as the counter and will start at 0:

    class WebBrowser:

    number_of_web_browsers = 0

    connected = True

    def __init__(self, page):

    self.history = [page]

    self.current_page = page

    self.is_incognito = False

  2. Modify the constructor to increment the counter each time a new instance is created by adding the line WebBrowser.number_of_web_browsers += 1. This increments the number_of_web_browsers attribute of our class by 1 and will be called each time a new instance is initialized:

    class WebBrowser:

    number_of_web_browsers = 0

    connected = True

    def __init__(self, page):

    self.history = [page]

    self.current_page = page

    self.is_incognito = False

    WebBrowser.number_of_web_browsers += 1

    Let's test it out:

  3. First, check that the number_of_web_browsers counter is at 0:

    >>> WebBrowser.number_of_web_browsers

    0

    >>>

  4. Next, instantiate a new object and check the counter:

    >>> opera = WebBrowser("opera.com")

    >>> WebBrowser.number_of_web_browsers

    1

    >>>

  5. The counter increments with every other instance we create. Check this, as follows:

    >>> edge = WebBrowser("microsoft.com")

    >>> WebBrowser.number_of_web_browsers

    2

    >>>

Besides the use cases we've seen, class attributes should be used when you have variables that are common to all instances, such as constants for the class.

Activity 30: Creating Class Attributes

Suppose you are designing a piece of software for an elevator company. A part of the software involves a safety mechanism to prevent the elevator from being used when it's filled past its capacity.

Define a class called Elevator, which will have a maximum occupancy of 8. The elevator should be initialized with the number of occupants. If the number of occupants exceeds the limit during initialization, it should print out a message indicating that the limit has been exceeded and only initialize how many occupants should step off the elevator.

The steps are as follows:

  1. Create a file named elevator.py.
  2. Declare the Elevator class by adding an occupancy limit class attribute.
  3. Add the initializer, which will check whether the occupancy limit will be exceeded, and print a message indicating how many people should alight.
  4. Finally, create a few instances after the class declaration to test the class out:

    elevator1 = Elevator(6)

    print("Elevator 1 occupants:", elevator1.occupants)

    elevator2 = Elevator(10)

    print("Elevator 2 occupants:", elevator2.occupants)

We can then test out our script by running python elevator.py in the terminal. The output should look like this:

Figure 7.3: Output of running the elevator.py script
Figure 7.3: Output of running the elevator.py script

Note

Solution for this activity can be found at page 289.

Class Versus Instance Methods

In this section, we will take a brief look at instance methods and cover class methods in detail.

Exercise 40: Creating Instance Methods

In this exercise, we will implement the navigate() and clear_history() methods for the WebBrowser class we defined in the previous section:

  1. Add the navigate() method to the WebBrowser class:

    class WebBrowser:

    def __init__(self, page):

    self.history = [page]

    self.current_page = page

    self.is_incognito = False

    def navigate(self, new_page):

    self.current_page = new_page

    if not self.is_incognito:

    self.history.append(new_page)

    Any call to navigate will the set the browser's current page to the new_page argument and then add it to the history if we're not in incognito mode (incognito mode in browsers prevents browsing history from being recorded).

  2. Calling navigate() on an instance should change current_page:

    >>> vivaldi = WebBrowser("gocampaign.org")

    >>> vivaldi.current_page

    'gocampaign.org'

    >>> vivaldi.navigate("reddit.com")

    >>> vivaldi.current_page

    'reddit.com'

    >>> vivaldi.history

    ['gocampaign.org', 'reddit.com']

    >>>

  3. Create the clear_history method, which will delete the browser's history:

    class WebBrowser:

    def __init__(self, page):

    self.history = [page]

    self.current_page = page

    self.is_incognito = False

    def navigate(self, new_page):

    self.current_page = new_page

    if not self.is_incognito:

    self.history.append(new_page)

    def clear_history(self):

    self.history[:-1] = []

    The clear_history method removes everything from the history list up to the last element, which is our current page. This leaves only our current page on the list.

  4. Add to the browser history by navigating to a couple of pages and then call the clear_history() method to see whether it works:

    >>> chrome = WebBrowser("example.net")

    >>> chrome.navigate("example2.net")

    >>> chrome.navigate("example3.net")

    >>> chrome.history

    ['example.net', 'example2.net', 'example3.net']

    >>> chrome.current_page

    'example3.net'

    >>> chrome.clear_history()

    >>> chrome.history

    ['example3.net']

    >>>

We mentioned in the previous chapter that instance methods must receive self as the first argument. This is because self refers to the current instance in the context. Despite not passing it as an argument when calling instance methods, our method calls execute without any error. How does this work?

Python passes the self argument implicitly. In the preceding code snippet, when we call chrome.clear_history(), Python essentially passes chrome in as an argument to the method; therefore, we don't need to explicitly pass in a value for self.

Such a method, one that takes an instance (self) as the first parameter, is referred to as a bound method. They are bound to that specific instance when it is created. In a sense, it can be thought of as every instance of a class having its own copy of the method that was defined in the class. If we print out the instance method of any instance, we'll see the following output:

>>> chrome.navigate

<bound method WebBrowser.navigate of <__main__.WebBrowser object at 0x107a9a390>>

>>> opera = WebBrowser("foobar.com")

>>> opera.navigate

<bound method WebBrowser.navigate of <__main__.WebBrowser object at 0x107a9a400>>

>>>

The output for chrome.navigate tells us that it is a bound method of an object in the memory location 0x107a9a390. The output of opera.navigate tells us that it is a bound method of an object at a different object at memory location 0x107a9a400. This shows us that the two instance methods are tied/bound to different objects.

Class Methods

This brings us to class methods. Class methods differ from instance methods in that they are bound to the class itself and not the instance. As such, they don't have access to instance attributes. Additionally, they can be called through the class itself and don't require the creation of an instance of the class.

Regarding instance methods, we saw that the first parameter is always an instance; with class methods, the first parameter is always the class itself, as we'll see in our examples.

One common use case for class methods is when you're making factory methods. A factory method is one that returns objects. They can be used for returning objects of a different type or with different attributes. Let's add a class method called with_incognito() to our WebBrowser class that initializes a web browser object in incognito mode:

class WebBrowser:

def __init__(self, page):

self.history = [page]

self.current_page = page

self.is_incognito = False

def navigate(self, new_page):

self.current_page = new_page

if not self.is_incognito:

self.history.append(new_page)

def clear_history(self):

self.history[:-1] = []

@classmethod

def with_incognito(cls, page):

instance = cls(page)

instance.is_incognito = True

instance.history = []

return instance

Our function definition begins with a peculiar piece of syntax, @classmethod. We won't go into the details on it, but all we need to know right now is that it tells Python to add the function below it as a class method. On the next line, we declare our function, which takes two arguments, cls and page. The cls argument refers to our WebBrowser class in this context. All class methods must have the class as the first argument. The name can be cls, which is the convention, or anything else, whether it be class_ or foobar. All that matters is that the first argument of the class method is reserved.

We pass the page argument during the instantiation of our WebBrowser object. In the function's body, we instantiate an object which we assign the name instance. We then change the incognito value of that instance to True and clear the history list. Finally, we return the instance we've created.

Exercise 41: Testing our Factory Method

In this exercise, we'll try out our factory method:

  1. Print out the class method. The output tells us that it is a bound method of the WebBrowser class. This illustrates what we said earlier regarding class methods and how they are bound to the class itself:

    >>> WebBrowser.with_incognito

    <bound method WebBrowser.with_incognito of <class '__main__.WebBrowser'>>

  2. Create a WebBrowser instance that starts off in incognito mode. Note that we call with_incognito through the class. Despite not passing the cls argument in this call, Python implicitly passes the WebBrowser class to the function. All we need to pass in is the page parameter.

    >>> chrome = WebBrowser.with_incognito("shady-website.com")

    >>> chrome.is_incognito

    True

  3. Print out the current page of our instance to check whether it was set:

    >>> chrome.current_page

    'shady-website.com'

  4. Confirm that the history was not tracked:

    >>> chrome.history

    []

    >>>

  5. Additionally, you can call class methods through instances for the same effect.

    >>> opera = WebBrowser("foobar.com")

    >>> netscape = opera.with_incognito("secret.net")

    >>> netscape.current_page

    'secret.net'

    >>> netscape.is_incognito

    True

    >>>

    Caution

    You should only call class methods through an instance in situations where it won't raise any confusion as to what kind of method it is you're calling (instance or class method).

Exercise 42: Accessing Class Attributes from within Class Methods

Class methods also have access to class attributes. They can get and set class attributes. Most browsers today have a geolocation API. We will simulate this functionality in our class.

In this exercise, we will create a geo_coordinates attribute in the WebBrowser class that holds the current latitude and longitude. We will also add a class method called change_geo_coordinates() that will change the coordinates when called:

  1. Add the geo_coordinates class attribute and change the geo_coordinates() class method, like so:

    class WebBrowser:

    geo_coordinates = {"lat": -4.764813, "lng": 16.131331 }

    def __init__(self, page):

    self.history = [page]

    self.current_page = page

    self.is_incognito = False

    def navigate(self, new_page):

    self.current_page = new_page

    if not self.is_incognito:

    self.history.append(new_page)

    def clear_history(self):

    self.history[:-1] = []

    @classmethod

    def with_incognito(cls, page):

    instance = cls(page)

    instance.is_incognito = True

    instance.history = []

    return instance

    @classmethod

    def change_geo_coordinates(cls, new_coordinates):

    if new_coordinates["lat"] > 90 or new_coordinates["lat"] < -90:

    print("Invalid value for latitude. Should be within the"

    " range from -90 to 90 degrees.")

    return None

    if new_coordinates["lng"] > 180 or new_coordinates["lng"] < -180:

    print("Invalid value for longitude. Should be within the"

    " range from -180 to 180 degrees.")

    return None

    cls.geo_coordinates = new_coordinates

    Our class method, change_geo_coordinates, takes the new_coordinates parameter, which is a dictionary. It checks whether the latitude and longitude provided in the parameters are valid and then changes the class attribute geo_coordinates to reflect the new coordinates that have been provided. We can test this out.

  2. Create a WebBrowser instance, firefox, and check its geocoordinates. It fetches the attribute from the class:

    >>> firefox = WebBrowser("www.org")

    >>> firefox.geo_coordinates

    {'lat': -4.764813, 'lng': 16.131331}

  3. Calling change_geo_coordinates on the class as we do in the next line changes the geo_coordinates attribute for all of the class's instances (since they fetch the attribute from the class), and hence this change reflects for the firefox instance:

    >>> WebBrowser.change_geo_coordinates({"lat": 31, "lng": 123})

    >>> firefox.geo_coordinates

    {'lat': 31, 'lng': 123}

    >>> WebBrowser.change_geo_coordinates({"lat": 31, "lng": 190})

    Invalid value for longitude. Should be within the range from -180 to 180 degrees.

    >>> WebBrowser.change_geo_coordinates({"lat": -100, "lng": 123})

    Invalid value for latitude. Should be within the range from -90 to 90 degrees.

    >>>

Encapsulation and Information Hiding

One of the key concepts of OOP is encapsulation. Encapsulation is the bundling of data with the methods that operate on that data. It's used to hide the internal state of an object by bundling together and providing methods that can get and set the object state through an interface. This hiding of the internal state of an object is what we refer to as information hiding.

With our WebBrowser class, when we called the navigate method as users, all we cared about was that it changed the current page. The class was a bundle of data and logic that gave us access to a uniform browser interface. The same is true for a real web browser. As users, we simply type in the URL and hit the Enter key, and it takes us to the new page. We don't care to know that the browser had to make a request to the server, wait for the response, render the resulting markup, apply styling, and download accompanying media along with it. The browser acts as a simple interface that allows us to interact with the internet. The processes behind all the steps it takes are hidden away from the users.

We use information hiding to abstract away irrelevant details about the class from users to prevent them from changing them, which would affect the functionality of our class.

In Python, information hiding is accomplished by marking attributes as private or protected:

  • private attributes should only be used inside the class definition and shouldn't be accessed externally.
  • protected attributes are similar to private ones, but can only be used in very specific contexts.

By default, all attributes in Python are public.

In most languages, these attribute access modifiers are denoted by the keyword private, public, or protected. Python, however, simply implements these in the attribute names themselves.

All Python attributes are public by default and need no special naming or declaration:

class Car:

def __init__(self):

self.speed = 300

self.color = "black"

For protected attributes, we prefix the attribute name with an underscore, _, to show that it's protected:

class Car:

def __init__(self):

self._speed = 300

self._color = "black"

Doing this doesn't change the class user's ability to change the attribute. It's simply a marker letting them know not to access or change the attribute from outside the class or its children. The interpreter enforces no actual restrictions to enforce this. You can still change protected attributes:

>>> car = Car()

>>> car._speed

300

>>> car._speed = 400

>>> car._speed

400

>>>

While it may seem that marking attribute names as protected is useless since it doesn't impose any restrictions, it is good practice to do it to let the users of the class know it's a protected attribute that is only meant to be used internally. It is up to them to follow convention and not assign or access protected attributes.

For private attributes, we prefix the attribute name with a double underscore __. This renders the attribute inaccessible from outside the class. The attribute can only be gotten and set from within the class:

class Car:

def __init__(self):

self.__speed = 300

self.__color = "black"

def change_speed(self, new_speed):

self.__speed = new_speed

def get_speed(self):

return self.__speed

If we try accessing any of these attributes from outside the class, we'll get an error:

>>> car = Car()

>>> car.__speed

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

AttributeError: 'Car' object has no attribute '__speed'

>>>

To change the private attribute __speed, we need to use the defined setter method change_speed. Similarly, we can use the get_speed getter method to get the speed attribute from outside the class if need be:

>>> car.get_speed()

300

>>> car.change_speed(120)

>>> car.get_speed()

120

>>>

Activity 31: Creating Class Methods and Using Information Hiding

Suppose you work for an electronics company that has a new MusicPlayer device that it wants to release to the market. The software for this device needs to support over-the-air updates that enables users to listen to their favorite tunes painlessly.

Create a class that represents a portable music player, MusicPlayer. The MusicPlayer class should have a play method, which sets the first track from the list of tracks as currently playing. The list of tracks should be a private attribute. Additionally, it should have a firmware version attribute and an update firmware class method that updates the firmware version.

The steps are as follows:

  1. Create a file named musicplayer.py.
  2. Define the MusicPlayer class by adding the firmware version class attribute.
  3. Define the initializer method and pre-populate the track list with a few songs. Make sure the music track's store is private.
  4. Define the play method, which sets the current_track attribute to the first item in the track's list.
  5. Define the list tracks method, which returns the list of tracks in the MusicPlayer.
  6. Finally, we'll add the update firmware version method, which checks for whether the new version being provided is more recent than the current firmware version before updating.
  7. We can then add a few test lines and run the script:

    player = MusicPlayer()

    print("Tracks currently on device:", player.list_tracks())

    MusicPlayer.update_firmware(2.0)

    print("Updated player firmware version to", player.firmware_version)

    player.play()

    print("Currently playing", f"'{player.current_track}'")

We can run the script by running python musicplayer.py in the terminal. The output should look like this:

Figure 7.4: Output of running the musicplayer.py script
Figure 7.4: Output of running the musicplayer.py script

Note

Solution for this activity can be found at page 290.

The following table compares instance attributes with class attributes:

Figure 7.5: Instance versus class attributes
Figure 7.5: Instance versus class attributes

The following table compares instance methods with class methods:

Figure 7.6: Instance versus class methods
Figure 7.6: Instance versus class methods

Class Inheritance

A key feature of object-oriented programming is inheritance. Inheritance is a mechanism that allows for a class's implementation to be derived from another class's implementation. This subclass/derived/child class inherits all of the attributes and methods of the superclass/base/parent class:

Figure 7.7: Inheritance in classes
Figure 7.7: Inheritance in classes

A practical real-world example of inheritance can be thought of with big cats. Cheetahs, leopards, tigers, and lions are all cats. They all share the same properties that are common to cats such as mass, lifespan, speed, and behaviors such as making vocalizations and hunting, among others. If we were to implement a Leopard, Cheetah, or Lion class, we would define one Cat class that has all of these properties and then derive the Leopard, Lion, and Cheetah classes from this Cat class since they all share these same properties. This would be inheritance.

We use inheritance because it confers the following benefits:

  • It makes our code more reusable. For example, with our Cat class example, we don't have to repeat the properties that each of the Lion, Cheetah, and Leopard classes possess; we can simply define them once in the Cat class and inherit the functionality in the derived classes. This also reduces code duplication.
  • Inheritance also makes it easier to extend functionality since a method or attribute added to a base class automatically gets applied to all of its subclasses. For example, defining a spots attribute in the Cat class automatically avails cheetah and leopard subclasses with the same attribute.
  • Inheritance adds flexibility to our code. Any place where a superclass instance is being used, a subclass instance can be substituted for the same effect. For example, at any place where a Cat instance would be used in our code, a Leopard, Lion, or Cheetah instance can be substituted since they're all cats.

The Python syntax for inheritance is very minimal. You define the class as usual, but then you can pass in the base class as a parameter. As we'll see later on, you can pass multiple base classes for cases where you want multiple inheritance, like so:

class Subclass(Superclass):

pass

Exercise 43: Implementing Class Inheritance

In this exercise, we'll define the Cat class from which we'll derive our other big cats. The class will have the methods vocalize and print_facts, and the attributes mass, lifespan, and speed.

The constructor method will take the arguments mass, lifespan, and speed from which it will add the attributes mass_in_kg, lifespan_in_years, and speed_in_kph to the object.

The vocalize method will print out Chuff, a non-threatening vocalization that's common to several big cats. The print_facts method will print out facts about the cat:

  1. Define the Cat class:

    class Cat:

    def __init__(self, mass, lifespan, speed):

    self.mass_in_kg = mass

    self.lifespan_in_years = lifespan

    self.speed_in_kph = speed

    def vocalize(self):

    print("Chuff")

    def print_facts(self):

    print(f"The {type(self).__name__.lower()} "

    f"weighs {self.mass_in_kg}kg,"

    f" has a lifespan of {self.lifespan_in_years} years and "

    f"can run at a maximum speed of {self.speed_in_kph}kph.")

    Note

    The line type(self).__name__ means that we want the name of the current class of the object, in this case, Cat. We then call str.lower() on the name in our example.

  2. Instantiate a cat instance and interact with the different methods and attributes it has:

    >>> cat = Cat(4, 18, 48)

    >>> cat.vocalize()

    Chuff

    >>> cat.print_facts()

    The cat weighs 4kg, has a lifespan of 18 years and can run at a maximum speed of 48kph.

    >>>

  3. Create the subclasses Leopard, Cheetah, and Lion, which will inherit from the Cat class:

    class Cat:

    def __init__(self, mass, lifespan, speed):

    self.mass_in_kg = mass

    self.lifespan_in_years = lifespan

    self.speed_in_kph = speed

    def vocalize(self):

    print("Chuff")

    def print_facts(self):

    print(f"The {type(self).__name__.lower()} "

    f"weighs {self.mass_in_kg}kg,"

    f" has a lifespan of {self.lifespan_in_years} years and "

    f"can run at a maximum speed of {self.speed_in_kph}kph.")

    class Cheetah(Cat):

    pass

    class Lion(Cat):

    pass

    class Leopard(Cat):

    pass

  4. Instantiate the new Leopard, Cheetah, and Lion classes.

    Despite not adding any methods or attributes to these new classes, if we instantiate them, we'll need to pass in the same arguments that we do when instantiating the Cat class. The methods and attributes our instance will have will be identical to a Cat class instance:

    >>> cheetah = Cheetah(72, 12, 120)

    >>> lion = Lion(190, 14, 80)

    >>> leopard = Leopard(90, 17, 58)

    >>> cheetah.print_facts()

    The cheetah weighs 72kg, has a lifespan of 12 years and can run at a maximum speed of 120kph.

    >>> lion.print_facts()

    The lion weighs 190kg, has a lifespan of 14 years and can run at a maximum speed of 80kph.

    >>> leopard.print_facts()

    The leopard weighs 90kg, has a lifespan of 17 years and can run at a maximum speed of 58kph.

    >>>

    As you can see, our subclasses have automatically inherited all of the attributes and methods of the Cat class. We have a slight issue on our hands, though.

  5. If we call vocalize on our instances, they all have the same behavior:

    >>> cheetah.vocalize()

    Chuff

    >>> lion.vocalize()

    Chuff

    >>> leopard.vocalize()

    Chuff

    >>>

    In reality, cheetahs make a chirrup, bird-like sound, while lions and leopards roar. We can rectify this by overriding the method in our class. Overriding means redefining the implementation of a method defined in a superclass to add or change a subclass's functionality.

  6. Override the vocalize method for our subclasses:

    class Cat:

    def __init__(self, mass, lifespan, speed):

    self.mass_in_kg = mass

    self.lifespan_in_years = lifespan

    self.speed_in_kph = speed

    def vocalize(self):

    print("Chuff")

    def print_facts(self):

    print(f"The {type(self).__name__.lower()} "

    f"weighs {self.mass_in_kg}kg,"

    f" has a lifespan of {self.lifespan_in_years} years and "

    f"can run at a maximum speed of {self.speed_in_kph}kph.")

    class Cheetah(Cat):

    def vocalize(self):

    print("Chirrup")

    class Lion(Cat):

    def vocalize(self):

    print("ROAR")

    class Leopard(Cat):

    def vocalize(self):

    print("Roar")

  7. If we call the vocalize method now, we should get different outputs:

    >>> cheetah.vocalize()

    Chirrup

    >>> lion.vocalize()

    ROAR

    >>> leopard.vocalize()

    Roar

    >>>

We'll be taking a look at overriding in more depth in the next section.

Overriding __init__()

In the previous topic, we overrode the vocalize() method of our Cat base class in our Cheetah, Lion, and Leopard subclasses. In this topic, we'll see how to override the __init__() method.

A lot of big cats have a pattern in their coat; they have spots or stripes. Let's add this to our Cheetah subclass.

Exercise 44: Overriding the __init__ Method to Add an Attribute

In this exercise, we'll override the __init__ method and add a spotted_coat attribute:

  1. Override the initializer method and add the spotted_coat attribute:

    class Cat:

    def __init__(self, mass, lifespan, speed):

    self.mass_in_kg = mass

    self.lifespan_in_years = lifespan

    self.speed_in_kph = speed

    def vocalize(self):

    print("Chuff")

    def print_facts(self):

    print(f"The {type(self).__name__.lower()} "

    f"weighs {self.mass_in_kg}kg,"

    f" has a lifespan of {self.lifespan_in_years} years and "

    f"can run at a maximum speed of {self.speed_in_kph}kph.")

    class Cheetah(Cat):

    def __init__(self, mass, lifespan, speed):

    self.spotted_coat = True

    def vocalize(self):

    print("Chirrup")

    Unfortunately, this overwrites the previous implementation and replaces it with our new one; so, when we initialize the Cheetah subclass, it won't add the mass_in_kg, lifespan_in_years, and speed_in_kph attributes. It will only add the spotted_coat attribute to the instance.

  2. Initialize the newly modified Cheetah class. It should raise an error upon trying to access the original attributes that it had before we overrode the __init__() method:

    >>> cheetah = Cheetah(72, 12, 120)

    >>> cheetah.mass_in_kg

    Traceback (most recent call last):

    File "<stdin>", line 1, in <module>

    AttributeError: 'Cheetah' object has no attribute 'mass_in_kg'

    >>> cheetah.lifespan_in_years

    Traceback (most recent call last):

    File "<stdin>", line 1, in <module>

    AttributeError: 'Cheetah' object has no attribute 'lifespan_in_years'

    >>> cheetah.speed_in_kph

    Traceback (most recent call last):

    File "<stdin>", line 1, in <module>

    AttributeError: 'Cheetah' object has no attribute 'speed_in_kph'

    >>> cheetah.spotted_coat

    True

    >>>

    What we can do is invoke the __init__ method of the Cat class inside the Cheetah subclass's __init__ method before adding the spotted_coat attribute. To do this, we can call Cat.__init__(self, mass, lifespan, speed), which calls the superclass's initializer with the required arguments.

  3. Call the superclass's initializer method in the Cheetah subclass initializer:

    class Cheetah(Cat):

    def __init__(self, mass, lifespan, speed):

    Cat.__init__(self, mass, lifespan, speed)

    self.spotted_coat = True

    def vocalize(self):

    print("Chirrup")

    However, doing this hardcodes the superclass name, and in case we need to change the name of the Cat class, we'd have to change it in multiple places. Python provides a cleaner way of doing this through the built-in super() method. We use super() to access inherited methods from a parent class that has been overwritten in the child class.

  4. Call the superclass's initializer method by using the super() method:

    class Cheetah(Cat):

    def __init__(self, mass, lifespan, speed):

    super().__init__(mass, lifespan, speed)

    self.spotted_coat = True

    def vocalize(self):

    print("Chirrup")

  5. When we instantiate a Cheetah instance, we see that our Cat superclass implementation is preserved:

    >>> cheetah = Cheetah(72,12,120)

    >>> cheetah.print_facts()

    The cheetah weighs 72kg, has a lifespan of 12 years and can run at a maximum speed of 120kph.

    >>>

    At the same time, our new implementation is also added:

    >>> cheetah.spotted_coat

    True

    >>>

Commonly Overridden Methods

As you may have noticed, special methods in Python classes are always prefixed and suffixed with double underscores, for example, __init__. They are known as Dunder (double underscore) or magic methods.

Besides the __init__ method, there are other magic methods in Python that you can override to customize your class and add custom functionality, such as changing what the output of your printed object looks like or how your classes are compared.

We will only be going over the method that defines what is output when print is called on your object, __str__(), and the method that's called when an object is destroyed, __del__().

Note

Special methods in Python classes are always prefixed and suffixed with double underscores. You can find the documentation for the rest of the special methods Python provides at https://docs.python.org/3/reference/datamodel.html.

The __str__() Method

Every object in Python has the __str__() method by default. It is called every time print() is called on an object in Python to retrieve the string containing the readable representation of the object.

Let's replace the print_facts() method of the Cat class with this method:

class Cat:

def __init__(self, mass, lifespan, speed):

self.mass_in_kg = mass

self.lifespan_in_years = lifespan

self.speed_in_kph = speed

def vocalize(self):

print("Chuff")

def __str__(self):

return f"The {type(self).__name__.lower()} "

f"weighs {self.mass_in_kg}kg,"

f" has a lifespan of {self.lifespan_in_years} years and "

f"can run at a maximum speed of {self.speed_in_kph}kph."

Now, when we call print() on any Cat instance or Cat subclass instance, it should have the same result as when we were calling print_facts():

>>> cheetah = Cheetah(72, 12, 120)

>>> print(cheetah)

The cheetah weighs 72kg, has a lifespan of 12 years and can run at a maximum speed of 120kph.

>>>

The __del__() Method

The __del__() method is the destructor method. The destructor method is called whenever an object gets destroyed:

class Cheetah(Cat):

def __init__(self, mass, lifespan, speed):

super().__init__(mass, lifespan, speed)

self.spotted_coat = True

def vocalize(self):

print("Chirrup")

def __del__(self):

print("No animals were harmed in the deletion of this instance")

If we call del on a Cheetah instance, it should print out that message:

>>> cheetah = Cheetah(72, 12, 120)

>>> del cheetah

No animals were harmed in the deletion of this instance

>>> cheetah

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

NameError: name 'cheetah' is not defined

>>>

Activity 32: Overriding Methods

Suppose you work for a wildlife conservation organization. You are working on creating a system to educate the general public about different animals and get them more interested in conservation.

Create a Tiger class that inherits from the Cat class and has a new coat pattern attribute. Change the behavior of instances of the Tiger class to include this coat pattern fact when they're printed.

The steps are as follows:

  1. Create the tiger.py file.
  2. Define the Cat class.
  3. Define the Tiger class that inherits from the Cat class. Override its initializer and add a coat_pattern attribute.
  4. Override the __str__() method and modify it to include mention of the coat pattern.
  5. Initialize an instance of the Tiger class. Calling print on the instance should display facts about the tiger. It should look like this:

    The tiger weighs 310kg, has a lifespan of 26 years and can run at a maximum speed of 65kph. It also has a striped coat.

    Note

    Solution for this activity can be found at page 291.

Multiple Inheritance

Multiple inheritance is a feature that allows you to inherit attributes and methods from more than one class. The most common use case for multiple inheritance is for mixins. Mixins are classes that have methods/attributes that are meant to be used by other functions. For example, a Logger class would have a log() method that writes to a logfile, and when added to your classes as a mixin, would give them that same capability.

The following is the syntax for multiple inheritance:

class Subclass(Superclass1, Superclass2):

pass

The subclass inherits all of the features of both superclasses.

Exercise 45: Implementing Multiple Inheritance

In the real world, lions and tigers can naturally mate to create a hybrid known as a liger or a tigon. Ligers are much larger than either lions or tigers, they are social like lions, have stripes, and, just like tigers, they like swimming. We're going to create a Liger class that inherits from both the Lion and Tiger class we're going to define.

In this exercise, we will learn how to implement multiple inheritance:

  1. Define the Lion and Tiger classes. For simplicity, we'll hardcode the mass, lifespan, and speed attributes. They'll both inherit from the Cat class:

    class Cat:

    def __init__(self, mass, lifespan, speed):

    self.mass_in_kg = mass

    self.lifespan_in_years = lifespan

    self.speed_in_kph = speed

    def vocalize(self):

    print("Chuff")

    def __str__(self):

    return f"The {type(self).__name__.lower()} "

    f"weighs {self.mass_in_kg}kg,"

    f" has a lifespan of {self.lifespan_in_years} years and "

    f"can run at a maximum speed of {self.speed_in_kph}kph."

    class Lion(Cat):

    def __init__(self, mass=190, lifespan=14, speed=80):

    super().__init__(mass, lifespan, speed)

    self.is_social = True

    def vocalize(self):

    print("ROAR")

    class Tiger(Cat):

    def __init__(self, mass=310, lifespan=26, speed=65):

    super().__init__(mass, lifespan, speed)

    self.coat_pattern = "striped"

    def swim(self):

    print("Splash splash")

    def vocalize(self):

    print("ROAR")

  2. Then, define the Liger class, which will inherit from both the Tiger and Lion classes:

    class Liger(Lion, Tiger):

    pass

  3. On testing it out, we should see that the Liger class has inherited attributes from both the Lion and Tiger classes. The Liger class has both the coat_pattern attribute and swim() method of the Tiger class and the is_social attribute of the Lion class:

    >>> liger = Liger()

    >>> liger.swim()

    Splash splash

    >>> liger.is_social

    True

    >>> liger.coat_pattern

    'striped'

    >>>

Activity 33: Practicing Multiple Inheritance

It's the year 2000. You're working for a mobile phone company and have been tasked with modeling out the software for a cutting-edge phone that will have a built-in camera: a camera phone.

Create a class called Camera and a class called MobilePhone that will be the base classes of a derived class called CameraPhone. The CameraPhone class should be initialized with the memory attribute and should have a take_picture() method that prints out the message, Say cheese!.

The steps are as follows:

  1. Create a Camera class that has a take_picture() method.
  2. Create a MobilePhone class that will be initialized with a memory attribute.
  3. Create a CameraPhone class that inherits from both the MobilePhone and Camera classes.
  4. Initialize an instance of the CameraPhone class. Calling the take_picture() method on the instance should have an output that looks like this. Also, print the memory attribute:

    Say cheese!

    200KB

    Note

    Solution for this activity can be found at page 292.

Summary

In this chapter, we have begun our journey into OOP. OOP makes code more reusable; it makes it easier to design software; it makes code easier to test, debug, and maintain; and it adds a form of security to the data in an application. The behaviors of an object are known as methods, and you can add a method to a class by defining a function inside it. To be bound to your objects, this function needs to take in the argument self. We also covered class attributes and class methods in detail. We also took a look at encapsulation and the keywords that enable information hiding in Python. Information hiding is used to abstract away irrelevant details about the class from users. This chapter also covered inheritance in detail. We saw how to have a derived class inherit from a single base class, as well as multiple base classes. We also saw how to override methods: specifically, the __init__(), __str__(), and __del__() methods. This chapter completes our journey into object-oriented programming with Python.

In the next chapter, we will cover Python modules and packages in detail. We will also take a look at how to handle different types of files and related file operations.

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

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