44.1. What is an Object?
Python is an object-oriented programming language and that means we work primarily with objects
. In this lesson, we’ll take a closer look at what an object is.
Documentation
What Is an Object?
Objects can be a little confusing, but a good way to think about objects is that they are entities encompassing data and functionality. Let’s take a look at the built-in types we’ve been using to look at the data and functionality encompassed by them. Custom object types are more complex than the built-in types, but looking at the primitive types will help us understand objects from a high level.
For strings (the str
type), the primary data that we interact with is the string itself, but that doesn’t mean it’s the only value a string encompasses. When we talk about the functionality an object encompasses, we mean the methods that the object has access to. We’ve seen a lot of methods on strings, such as lower
and upper
.
Thankfully, we can see everything an object encompasses by using the dir built-in function. Let’s take a look at a string in the REPL:
$ python3.7
Python 3.7.6 (default, Jan 30 2020, 15:46:02)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> my_str = "Test String"
>>> dir(my_str)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr
ibute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '_
_len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rm
ul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode',
'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit'
, 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstri
p', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitline
s', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
The dir function returns a list of all of the variables and functions that the object encompasses. We can chain any of these items off of our object and it will return a value. That value might be a method.
>>> my_str.__doc__
"str(object='') -> str\nstr(bytes_or_buffer[, encoding[, errors]]) -> str\n\nCreate a new string object from the given object. If encoding or\nerrors is specified, then the object must expose a data buffer\nthat will be decoded using the given encoding and error handler.\nOtherwise, returns the result of object.__str__() (if defined)\nor repr(object).\nencoding defaults to sys.getdefaultencoding().\nerrors defaults to 'strict'."
>>> my_str.isdigit
<built-in method isdigit of str object at 0x7f7fc84c11f0>
>>> my_str.isdigit()
False
We can see the documentation for the str
type and even access the methods from the object. A good way to tell if something is an object is to try to assign it to a variable. All objects can be assigned to variables.
44.2. Creating and Using Python Classes
The next step in our programming journey requires us to think about how we can model concepts from our problem. To do that, we’ll often use classes to create completely new data types. In this lesson, we’ll create our very first class and learn how to work with its data and functionality.
Python Documentation
Defining New Types
Up to this point, we’ve been working with the built-in types that Python provides (e.g. str
, int
, float
), but when we’re modeling problems in our programs we often want more complex objects that fit our specific problem’s domain. For instance, if we were writing a program to model information about vehicles for an automotive shop, then it would make sense for us to have an object type that represents a vehicle. This is where we will start working with classes.
From this point on, most of the code that we’ll be writing will be in files. Let’s create a python_objects
directory to hold these files that are only there to facilitate learning.
$ mkdir ~/python_objects
$ cd ~/python_objects
Creating Our First Class
For this lesson, we’ll use a file called vehicle.py
. Our goal is to model a vehicle that has tires and an engine. To create a class
we use the class keyword, followed by a name for the class, starting with a capital letter. Let’s create our first class, the Vehicle
class:
~/python_objects/vehicle.py
class Vehicle:
"""
Docstring describing the class
"""
def __init__(self):
"""
Docstring describing the method
"""
pass
This is an incredibly simple class. A few things to note here are that by adding a triple-quoted string right under the definition of the class, and also right under the definition of a method or function, we can add documentation. This documentation is nice because we can add examples in this string to run as tests to help ensure our documentation stays up-to-date with the implementation.
A method is a function defined within the context of an object, and Python classes can define special functions that start with double underscores __
, such as the __init__
method. This method is the initializer for our class, and it is where we customize what happens when a new instance is being created. In practice, this method will usually just set attributes on the instance. The initializer is what is used when we create a new version of our class by running code like this:
>>> my_vehicle = Vehicle()
We would like our Vehicle
class to hold a few pieces of data such as the tires and an engine. For the time being, we’re going to have those be a list containing a string for the tires and a string for the engine. Let’s modify our __init__
method to have the engine
and tires
parameters:
~/python_objects/vehicle.py
class Vehicle:
"""
Vehicle models a vehicle w/ tires and an engine
"""
def __init__(self, engine, tires):
self.engine = engine
self.tires = tires
What Is self?
A big change from writing functions to writing methods is the presence of self
. This variable references the individual instance of the class that we’re working with. The Vehicle
class holds onto the information about vehicles within our program, where an instance of the Vehicle
class could represent a specific vehicle like my Honda Civic. Let’s load our class into the REPL using python3.7 -i vehicle.py
, and then create a specific instance of my Honda Civic.
$ python3.7 -i vehicle.py
>>> civic = Vehicle('4-cylinder', ['front-driver', 'front-passenger', 'rear-driver', 'rear-passenger'])
>>> civic.tires
['front-driver', 'front-passenger', 'rear-driver', 'rear-passenger']
>>> civic.engine
'4-cylinder'
Once we have our instance, we can access our internal attributes by using a period (.). Attributes are variables attached to the instance. Our civic
variable has an engine
attribute, which just means that engine
is one of its internal variables.
Defining a Custom Method
The last thing that we’ll do to round out the first rendition of our class is to define a method that prints a description of the vehicle to the screen.
~/python_objects/vehicle.py
class Vehicle:
"""
Vehicle models a vehicle w/ tires and an engine
"""
def __init__(self, engine, tires):
self.engine = engine
self.tires = tires
def description(self):
print(f"A vehicle with an {self.engine} engine, and {self.tires} tires")
Our description
method doesn’t have any actual arguments, but we pass the instance in as self
. From there, we can access the instance’s attributes by calling self.attribute_name
.
Let’s use this new method:
$ python3.7 -i vehicle.py
>>> civic = Vehicle('4-cylinder', ['front-driver', 'front-passenger', 'rear-driver', 'rear-passenger'])
>>> civic.engine
'4-cylinder'
>>> civic.tires
['front-driver', 'front-passenger', 'rear-driver', 'rear-passenger']
>>> civic.description
<bound method Vehicle.description of <__main__.Vehicle object at 0x7fb5f3fbbda0>>
>>> civic.description()
A vehicle with a 4-cylinder engine, and ['front-driver', 'front-passenger', 'rear-driver', 'rear-passenger'] tires
Just like a normal function, if we don’t use parenthesis, the method won’t execute.
Adding and Removing Attributes from Instances
We’ve seen how to define attributes as part of our instance initialization code, but an instance of a custom class also acts as a namespace for any attribute we want. This means that after we create an instance of a custom class, we can add attributes to it in the same way we assign a new variable. We just need to chain the attribute off of our instance’s identifier. Let’s add a serial_number
(attribute) to my civic
(identifier).
>>> civic.serial_number = '1234'
>>> civic.serial_number
'1234'
We can remove attributes from an instance of a class using the del
keyword, just like we would to delete a variable. Remember, we need to be accessing the attribute and not just pass in our object.
>>> del civic.serial_number
>>> civic.serial_number
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Vehicle' object has no attribute 'serial_number'
44.3. Custom Constructors, Class Methods, and Decorators
There’s a lot to learn when it comes to creating and building robust classes. In this lesson, we continue to learn about some of the tools at our disposal when creating classes: custom constructors and class methods.
Documentation
Custom Class Constructors
Unlike other languages like Java, Python doesn’t provide a way for us to create multiple constructor methods. Instead, we get a single constructor method that we can customize, the __init__
method. We’ve already customized this for our Vehicle
class. This method has a default implementation that takes no arguments, so by defining this in our class, we’ve created a custom constructor.
Using @classmethod to Create Convenience Constructor Methods
Although creating multiple different constructors isn’t a feature of Python, it doesn’t mean we can’t do something similar. If we want another way to construct a Vehicle
object with some preset values, we can create convenience methods using what is known as a class method. A class method is a function attached to the class itself, not an instance of the class, and it has access to any class-level attributes. To create a class method, we need to use what is known as a decorator. Decorators are functions or classes that we use to add additional functionality to a function by prefixing the decorator’s name with an at-sign (@
) and putting it on the line above our function or method definition. This sounds confusing, but remember back to our look at higher-order functions. A decorator takes a function and returns another modified function in its place. For the purposes of the PCAP, we only need to know how to use one specific decorator so that we can add class methods to our classes: the @classmethod
decorator. Let’s add a method to our Vehicle
class that will allow us to create a bicycle (which has two wheels, and no engine).
~/python_objects/vehicle.py
class Vehicle:
"""
Vehicle models a vehicle w/ tires and an engine
"""
default_tire = 'tire'
def __init__(self, engine, tires):
self.engine = engine
self.tires = tires
@classmethod
def bicycle(cls, tires=None):
if not tires:
tires = [cls.default_tire, cls.default_tire]
return cls(None, tires)
def description(self):
print(f"A vehicle with an {self.engine} engine, and {self.tires} tires")
Notice we added a class-level variable called default_tire
. This variable is set on the class itself and will also be available to instances of the class. By decorating the bicycle
as a @classmethod
, we’re able to call Vehicle.bicycle()
, and the class itself will be passed in as the implicit cls
argument (this name is a convention, not a required name). Because the class itself (Vehicle)
is passed into the method as the cls
variable, that means when we call cls()
, it is equivalent to doing Vehicle()
and will invoke the __init__
method. It’s beneficial to use the cls
variable instead of the class name, because if we ever change the name of the class, then we won’t need to modify this function. If no argument is passed in for the tires
parameter, then we’ll create a default list containing the value of the default_tire
class attribute two times. Let’s load this file into the REPL and see if it works:
$ python3.7 -i vehicle.py
>>> bike = Vehicle.bicycle()
>>> bike
<__main__.Vehicle object at 0x7f947c0f7750>
>>> bike.description()
A vehicle with an None engine, and ['tire', 'tire'] tires
>>> bike.engine
>>> bike.tires
['tire', 'tire']
As we start modeling more and more concepts, there will be more situations where we’ll want to use class methods to perform actions that require information available to only the class and doesn’t require any instance information.
44.4. Inheritance and Super
Our Vehicle.bicycle
class method does a good job of creating a vehicle that looks like a bicycle
, but should we have a Bicycle class instead? Because a bicycle is a type of vehicle, we can leverage the code that exists in the Vehicle
class by creating a new class that inherits from the Vehicle
class. In this lesson, we’ll learn about inheritance, one of the core tenants of object-oriented programming (OOP).
Documentation
Using Inheritance to Customize an Existing Class
Our existing Vehicle
implementation does exactly what we need it to do for a general vehicle, but there are other, more specific types of vehicles such as cars, trucks, boats, and bicycles. If we wanted to model these other types of vehicles, we could use our existing Vehicle
class as a starting point by inheriting its existing implementation. Let’s add a new Bicycle
class to a new file called bicycle.py
.
~/python_objects/bicycle.py
from vehicle import Vehicle
class Bicycle(Vehicle):
pass
By passing in the Vehicle
class to our class definition for Bicycle
, we’re specifying that our class is a subclass of Vehicle
. As it stands right now, the Bicycle
class will behave exactly like the Vehicle
type. From here, we can add more functionality and internal states specific to a bicycle. The convenience method we added to Vehicle
essentially allows us to have a constructor that doesn’t accept an engine, since a bicycle doesn’t have an engine. That’s what the constructor for Bicycle
should do. Let’s customize the initializer to do this.
~/python_objects/bicycle.py
from vehicle import Vehicle
class Bicycle(Vehicle):
def __init__(self, tires=[]):
if not tires:
tires = [self.default_tire, self.default_tire]
self.tires = tires
Because Bicycle
is a subclass of Vehicle
, it already has access to the class-level variable default_tire
, so we don’t need to redefine that to use it within the __init__
method. Let’s use our class in the REPL to see if it is working correctly.
$ python -i bicycle.py
>>> bike = Bicycle()
>>> bike.tires
['tire', 'tire']
>>> custom_bike = Bicycle(['front-tire', 'back-tire'])
>>> custom_bike.description()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/cloud_user/python_objects/vehicle.py", line 18, in description
print(f"A vehicle with an {self.engine} engine, and {self.tires} tires")
AttributeError: 'Bicycle' object has no attribute 'engine'
This error makes sense. Our bicycle doesn’t have an engine. We’re going to need to customize the description
method to change the message. Looking at this error, does it make sense for Vehicle
to require an engine and tires? Not really. A bicycle doesn’t have an engine, and a boat doesn’t have tires, so neither of those should be required. One piece of information that does describe a vehicle could be distance_traveled
. Let’s make Vehicle
more abstract and have the description
only print out the distance traveled.
~/python_objects/vehicle.py
class Vehicle:
"""
Vehicle models a device that can be used to travel.
"""
def __init__(self, distance_traveled=0, unit='miles'):
self.distance_traveled = distance_traveled
self.unit = unit
def description(self):
print(f"A {self.__class__.__name__} that has traveled {self.distance_traveled} {self.unit}")
Now our Vehicle
class is much more generic and can be used as the parent class or base class for any more specific vehicle. We do need to change our Bicycle
implementation now, and we will want to make sure that we’re setting a distance_traveled
and unit
. Thankfully, we don’t need to redo these lines, because we can use super
. Let’s break down the expressions self.__class__.__name__
. The variable self
is an instance of Vehicle
in this case, but when this method is called from a subclass, then self
will be an instance of that class instead. We want to display the name of the class in our description
output, so we’ll access the __name__
attribute on the class itself. That will provide us the string value.
Using super()
When we want to customize a method written on a parent class without entirely replacing the method, then we’re able to invoke the parent class’s implementation of the method by calling the super()
function. We need to do this to change the Bicycle.__init__
method. A bicycle has tires, so we want that as another parameter in the initializer. Otherwise, we would like to have the initialization behave the same way as it does for Vehicle
. Let’s implement __init__
.
~/python_objects/bicycle.py
from vehicle import Vehicle
class Bicycle(Vehicle):
default_tire = 'tire'
def __init__(self, tires=[], distance_traveled=0, unit='mile'):
super().__init__(distance_traveled, unit)
if not tires:
tires = [self.default_tire, self.default_tire]
self.tires = tires
By calling super()
, we have access to the methods implemented in our parent class, Vehicle
. We’ll then call the __init__
method with the proper parameters. The self
, in the context of this call to __init__
, is our Bicycle
instance. So, this method call will set distance_traveled
(self.distance_traveled=0
) and unit
(self.unit = 'miles'
) on our Bicycle
class. We’re leveraging code from the parent class while adding a little more to the initialization of this new class.
Let’s take this into the REPL to see how it works.
$ python3.7
Python 3.7.6 (default, Jan 30 2020, 15:46:02)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from vehicle import Vehicle
>>> from bicycle import Bicycle
>>> vehicle = Vehicle()
>>> bike = Bicycle()
>>> vehicle.description()
A Vehicle that has traveled 0 miles
>>> bike.description()
A Bicycle that has traveled 0 mile
As we can see, description outputs something different for the Bicycle
without us even changing it because we wrote it in a generic, context-aware way. That being said, we would like to add information about our tires to the description
too, and once again this is a situation where we can leverage super. To make this a little quicker to test, we’ll also add a __name__
== "__main__"
condition with some code to demonstrate what we’re writing.
~/python_objects/bicycle.py
from vehicle import Vehicle
class Bicycle(Vehicle):
default_tire = 'tire'
def __init__(self, tires=[], distance_traveled=0, unit='mile'):
super().__init__(distance_traveled, unit)
if not tires:
tires = [self.default_tire, self.default_tire]
self.tires = tires
def description(self):
initial = super().description()
return f"{initial} on {len(self.tires)} tires."
if __name__ == "__main__":
bike = Bicycle()
print(bike.description())
It doesn’t really make sense for the call to description
to print the information. That just makes it hard to work with. What we’ve written here works a little better and allows us to control when we’re using the class that is going to be printed. We want the initial string provided by Vehicle
, and then we’ll customize it to show a little information about how many tires we have. Because of how Vehicle.description
is currently written, this won’t quite work the way we would like because its printing the description on two lines.
$ python3.7 bicycle.py
A Bicycle that has traveled 0 mile
None on 2 tires.
Let’s fix this by making Vehicle.description
return a string, rather than the side-effect of printing a message.
~/python_objects/vehicle.py
class Vehicle:
"""
Vehicle models a device that can be used to travel.
"""
def __init__(self, distance_traveled=0, unit='miles'):
self.distance_traveled = distance_traveled
self.unit = unit
def description(self):
return f"A {self.__class__.__name__} that has traveled {self.distance_traveled} {self.unit}"
Let’s run bicycle.py
one last time to see that the description is now on one line.
$ python3.7 bicycle.py
A Bicycle that has traveled 0 miles on 2 tires.
We’ve learned quite a bit in this lesson about inheritance and super, but also how not to design our objects. Sometimes our initial thoughts about our objects are just not right and can make working with the items harder than we originally imagined.
44.5. Single and Multiple Inheritance
Sometimes we have an object that makes sense to be a subclass of more than one other type. In these situations, we can use what is called multiple inheritance. Multiple inheritance is not something we use too often, but it is good to understand how it works. We’ll learn all about it in this lesson.
Documentation
Multiple Inheritance
Multiple inheritance allows us to inherit from multiple parent classes. This can be used to pull functionality from multiple different classes into a single class. To demonstrate multiple inheritance, we’re going to continue modeling vehicles starting with new classes for a Car
and a Boat
. These classes are very similar to what we did with Bicycle
, so we’ll quickly create these classes without much additional comment.
~/python_objects/car.py
from vehicle import Vehicle
class Car(Vehicle):
default_tire = 'tire'
def __init__(self, engine, tires=[], distance_traveled=0, unit='miles'):
super().__init__(distance_traveled, unit)
if not tires:
tires = [self.default_tire, self.default_tire]
self.tires = tires
self.engine = engine
def drive(self, distance):
self.distance_traveled += distance
~/python_objects/boat.py
from vehicle import Vehicle
class Boat(Vehicle):
def __init__(self, boat_type='sail', distance_traveled=0, unit='miles'):
super().__init__(distance_traveled, unit)
self.boat_type = boat_type
def voyage(self, distance):
self.distance_traveled += distance
def description(self):
initial = super().description()
return f"{initial} using a {self.boat_type}"
We can model another type of vehicle, called AmphibiousVehicle
, by inheriting from both a Car
and a Boat
. This is a type of vehicle that is both a car (so it can travel on land) and a boat (so it can travel through the water). To use multiple inheritance, we separate our parent classes with a comma in the same way that we would with function parameters. Here’s the initial version of our AmphibiousVehicle
(in amphibious_vehicle.py
):
~/python_objects/amphibious_vehicle.py
from boat import Boat
from car import Car
class AmphibiousVehicle(Car, Boat):
pass
Let’s load this class into the REPL to see what it attempts to do when we initialize a new one.
$ python -i amphibious_vehicle.py
>>> water_car = AmphibiousVehicle()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __init__() missing 1 required positional argument: 'engine'
>>>
By default, our class will try to use the method from the first class in the list of classes (Car
) that we inherit when we don’t customize our method. We’re going to want to customize __init__
to explicitly set our boat_type
to 'motor'
and then continue with our initialization using the code from the Car
class.
~/python_objects/amphibious_vehicle.py
from boat import Boat
from car import Car
class AmphibiousVehicle(Car, Boat):
def __init__(self, engine, tires=[], distance_traveled=0, unit='miles'):
super().__init__(engine, tires, distance_traveled, unit)
self.boat_type = 'motor'
def travel(self, land_distance=0, water_distance=0):
self.voyage(water_distance)
self.drive(land_distance)
Let’s try this again to see what happens.
$ python -i amphibious_vehicle.py
>>> water_car = AmphibiousVehicle('4 cylinder')
>>> water_car.description()
'A AmphibiousVehicle that has traveled miles miles using a motor'
There are some issues here. First, AmphibiousVehicle
isn’t quite what we’re going for when we print this out. Additionally, our distance_traveled
attribute is apparently being set to 'miles'
instead of 0
. To understand what is going on, we need to get a better understanding of the method resolution order when calling super
when using multiple inheritance.
Method Resolution Order
Method resolution order is a term for looking at how methods on an object are found and which ones are run. Thankfully, we can see the method resolution order (i.e. “MRO”) by accessing the __mro__
attribute on our AmphibiousVehicle
class.
>>> AmphibiousVehicle.__mro__
(<class '__main__.AmphibiousVehicle'>, <class 'car.Car'>, <class 'boat.Boat'>, <class 'vehicle.Vehicle'>, <class 'object'>)
This shows us that we will run what is found in AmphibiousVehicle
first, Car
second, Boat
third, Vehicle
fourth, and object
last (object is the default type that we inherit when we create a class). What this list doesn’t tell us is that when we call super
, it will call the method in all of our parent classes that implement it. To show this, let’s add some print lines into our parent class __init__
functions.
~/python_objects/boat.py
from vehicle import Vehicle
class Boat(Vehicle):
def __init__(self, boat_type='sail', distance_traveled=0, unit='miles'):
print(f"__init__ from Boat with distance_traveled: {distance_traveled} and {unit}")
super().__init__(distance_traveled, unit)
self.boat_type = boat_type
def voyage(self, distance):
self.distance_traveled += distance
def description(self):
initial = super().description()
return f"{initial} using a {self.boat_type}"
~/python_objects/car.py
from vehicle import Vehicle
class Car(Vehicle):
default_tire = 'tire'
def __init__(self, engine, tires=[], distance_traveled=0, unit='miles'):
print(f"__init__ from Car with distance_traveled: {distance_traveled} and {unit}")
super().__init__(distance_traveled, unit)
if not tires:
tires = [self.default_tire, self.default_tire]
self.tires = tires
self.engine = engine
def drive(self, distance):
self.distance_traveled += distance
~/python_objects/vehicle.py
class Vehicle:
"""
Vehicle models a device that can be used to travel.
"""
def __init__(self, distance_traveled=0, unit='miles'):
print(f"__init__ from Vehicle with distance_traveled: {distance_traveled} and {unit}")
self.distance_traveled = distance_traveled
self.unit = unit
def description(self):
return f"A {self.__class__.__name__} that has traveled {self.distance_traveled} {self.unit}"
Now if we initialize a new AmphibiousVehicle
, we should get more insight into how distance_traveled
is being set to 'miles'
.
$ python -i amphibious_vehicle.py
>>> water_car = AmphibiousVehicle('4-cylinder')
__init__ from Car with distance_traveled: 0 and miles
__init__ from Boat with distance_traveled: miles and miles
__init__ from Vehicle with distance_traveled: miles and miles
As we can see, we call super
one time, and yet both Car
and Boat
have their __init__
methods run. This is a little confusing because it’s not that we’re running both methods from AmphibiousVehicle
. It’s that Car.__init__
also calls super. Because self
is an AmphibiousVehicle
at the moment that super
is called from Car
, it calls __init__
from the next object in the method resolution order, which is Boat
. We’re then calling Boat.__init__
with only distance_traveled
and unit
as positional arguments.
If we run Boat(0, 'miles')
, this will give us a Boat
with distance_traveled
set to 'miles'
. How do we get around this? By adjusting our objects to be more flexible by using **kwargs
to capture extra keyword arguments and explicitly use keyword arguments when calling super().__init__
.
~/python_objects/amphibious_vehicle.py
from boat import Boat
from car import Car
class AmphibiousVehicle(Car, Boat):
def __init__(self, engine, tires=[], distance_traveled=0, unit="miles"):
super().__init__(
engine=engine, tires=tires, distance_traveled=distance_traveled, unit=unit
)
self.boat_type = "motor"
def travel(self, land_distance=0, water_distance=0):
self.voyage(water_distance)
self.drive(land_distance)
~/python_objects/boat.py
from vehicle import Vehicle
class Boat(Vehicle):
def __init__(self, boat_type="sail", distance_traveled=0, unit="miles", **kwargs):
super().__init__(distance_traveled=distance_traveled, unit=unit, **kwargs)
self.boat_type = boat_type
def voyage(self, distance):
self.distance_traveled += distance
def description(self):
initial = super().description()
return f"{initial} using a {self.boat_type}"
~/python_objects/car.py
from vehicle import Vehicle
class Car(Vehicle):
default_tire = "tire"
def __init__(self, engine, tires=[], distance_traveled=0, unit="miles", **kwargs):
super().__init__(distance_traveled=distance_traveled, unit=unit, **kwargs)
if not tires:
tires = [self.default_tire, self.default_tire]
self.tires = tires
self.engine = engine
def drive(self, distance):
self.distance_traveled += distance
~/python_objects/vehicle.py
class Vehicle:
"""
Vehicle models a device that can be used to travel.
"""
def __init__(self, distance_traveled=0, unit="miles", **kwargs):
self.distance_traveled = distance_traveled
self.unit = unit
def description(self):
return f"A {self.__class__.__name__} that has traveled {self.distance_traveled} {self.unit}"
Now that our initialization methods are more flexible, and we’re being more explicit, let’s try this again to see if our attributes are set properly.
$ python -i amphibious_vehicle.py
>>> water_car = AmphibiousVehicle('4 cylinder')
>>> water_car.description()
'A AmphibiousVehicle that has traveled 0 miles using a motor'
>>> water_car.travel(10, 15)
>>> water_car.description()
'A AmphibiousVehicle that has traveled 25 miles using a motor'
We’ve finally been able to use our class properly and call the travel
method. Because Boat
implements voyage
, and Car
implements drive
, we’re able to call each of those methods using super
. They will be dispatched to the proper parent class. This shows one of the important things to consider when working with multiple inheritance. Things work well if our parent classes don’t implement the same method names, but it can become a headache to debug issues when multiple classes implement the same method and also call super
.
44.6. Name Mangling
Python doesn’t really have the concept of private classes or instance variables. Data on an object can always be accessed explicitly. But by following some conventions, we can utilize a feature called name mangling to ensure that private data on a parent class isn’t overwritten if the subclass also has a variable with the same name.
Documentation
What Is Name Mangling?
Name mangling is something that allows the interpreter to replace special identifiers in our classes with ones that are specific to the class they’re written in. This isn’t something we’ll leverage often, but the official tutorial has a snippet of code that demonstrates this really well. Let’s create a file called mapping.py
to work with this example code (which can be found here).
~/python_objects/mapping.py
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # private copy of original update() method
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
There are some comments in this code that roughly explain what is going on, but the important points are this:
- In
Mapping
, we’re keeping a reference to the original version ofupdate
as__update
, so that we can use it within the__init__
method. By doing this, we’ll always use the original version of the method even if a subclass implements a different version ofupdate
(as theMappingSubclass
does). - The name mangling aspect of things is that
__update
becomes_Mapping__update
, even if we add an__update
identifier toMappingSubclass
.
Let’s add an __update
identifier to MappingSubclass
before we take a look at what is going on in the REPL.
~/python_objects/mapping.py
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # private copy of original update() method
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
def print_something(self):
print("Printing something")
__update = print_something
Let’s load this into the REPL and see the identifiers that exist on our classes.
$ python -i mapping.py
>>> Mapping.__update
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'Mapping' has no attribute '__update'
>>> dir(Mapping)
['_Mapping__update', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'update']
>>> dir(MappingSubclass)
['_MappingSubclass__update', '_Mapping__update', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'print_something', 'update']
Notice that Mapping
has a _Mapping__update
identifier, but not __update
, and MappingSubclass
has both _MappingSubclass__update
and _Mapping__update
. These are the rules for creating an identifier that will be name mangled:
- The name starts with at least two underscores (__).
- The name has at most one trailing underscore (_).
- The identifier must be defined in the definition of the class at the same level as methods.
44.7. Inspecting Objects
As we work with more and more custom classes, we will need to start inspecting the variables we have to see what information we have to work with. We won’t be able to keep the information about all of the types in our systems in our minds after a certain point, and knowing the tools we can use to get more information from our objects is very useful. In this lesson, we’ll learn about various built-in functions, methods, and attributes that we can use to get more information about classes and objects we’re working with.
Documentation
- Classes
- Special Attirbutes
- Basic Object Customization
- The str Method
- The type Function
- The hasattr Function
- The issubclass Function
- The isinstance Function
Inspecting Instances and Classes
There are two main things we’ll want to get more information about when we’re doing object-oriented programming:
- The classes
- The instances of those classes
By learning more about the classes that we’ll need to work with, we can have a better idea of how they were intended to be used. By learning more about the instances created as code where our system is running, we can better debug and understand what is going on as we interact with the objects. Let’s start by taking a look at how we can learn more about a class by loading our amphibious_vehicle.py
class into the REPL and taking a look at some of the “private” attributes and methods on the class.
$ python3.7 -i amphibious_vehicle.py
>>> AmphibiousVehicle.__bases__
(<class 'car.Car'>, <class 'boat.Boat'>)
>>> from vehicle import Vehicle
>>> Vehicle.__subclasses__()
[<class 'boat.Boat'>, <class 'car.Car'>]
>>> dir(AmphibiousVehicle)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'default_tire', 'description', 'drive', 'travel', 'voyage']
Notice that __bases__
doesn’t show up when we pass our class to the dir
function. Some of the special attributes that exist don’t show up when using dir
, and we just need to know them. We also have some functions we can use to get information about our classes:
- The hasattr function: Takes an object and a string with the name of the identifier we’d like to check for. It’s worth noting that if we pass in the class, it will check for class-level attributes, not instance-level attributes.
>>> from boat import Boat
>>> hasattr(Boat, 'boat_type')
False
>>> from car import Car
>>> hasattr(Car, 'default_tire')
True
- The issubclass function: Checks to see if the first class passed in is a subclass of the second class. The order is important.
>>> from vehicle import Vehicle
>>> issubclass(Boat, Vehicle)
True
>>> issubclass(Boat, AmphibiousVehicle)
False
>>> issubclass(AmphibiousVehicle, Boat)
True
- The isinstance function: Checks to see if an object is an instance of the given class. Note that an object is an instance of its class’s subclasses.
>>> from bicycle import Bicycle
>>> water_car = AmphibiousVehicle('4 cylinder')
>>> isinstance(water_car, Bicycle)
False
>>> isinstance(water_car, AmphibiousVehicle)
True
>>> isinstance(water_car, Boat)
True
- The dict attribute: Returns a dictionary (or dictionary-like object) containing all of the custom (i.e. writable) attributes on the object. This can be used on both classes and instances of classes. The result for a class is a bit weird looking, but notice that it only contains the methods and attributes we defined.
>>> water_car.__dict__
{'distance_traveled': 0, 'unit': 'miles', 'boat_type': 'motor', 'tires': ['tire', 'tire'], 'engine': '4 cylinder'}
>>> Boat.__dict__
mappingproxy({'__module__': 'boat', '__init__': <function Boat.__init__ at 0x7ff9228b9f80>, 'voyage': <function Boat.voyage at 0x7ff9228b9dd0>, 'description': <function Boat.description at 0x7ff922835050>, '__doc__': None})
- The type function: Returns the class used to create the object.
>>> type(water_car)
<class '__main__.AmphibiousVehicle'>
Notice that the class is __main__.AmphibiousVehicle
. This shows the value of the __module__
attribute for the class and then the class name. Normally, this will not be __main__
, it would be the module that defines the class. It’s __main__
right now because we launched the REPL using python3.7 -i amphibious_vehicle.py
. That means it interpreted the file, effectively running those lines in the REPL itself. If we access the __module__
attribute on a different class, we will see the name of the defining module.
>>> Boat.__module__
'boat'
Customizing Objects with str
In addition to being able to get information from classes and instances that we’re working with, we can also make our class instances present their information in a better way for various situations. The primary situation where we’ll customize our object’s behavior is when it’s converted to a string.
Let’s take a look at what an AmphibiousVehicle
looks like when converted to a string or returned.
>>> str(water_car)
'<__main__.AmphibiousVehicle object at 0x7ff92
283a6d0>'
This is not super helpful, but we can customize this output by defining the __str__
method. Let’s define this method to return the class name and the attributes currently on the instance, using the __dict__
attribute. We’ll also add a main section so that we can quickly test what this output will look like.
~/python_objects/amphibious_vehicle.py
from boat import Boat
from car import Car
class AmphibiousVehicle(Car, Boat):
def __init__(self, engine, tires=[], distance_traveled=0, unit="miles"):
super().__init__(
engine=engine, tires=tires, distance_traveled=distance_traveled, unit=unit,
)
self.boat_type = "motor"
def travel(self, land_distance=0, water_distance=0):
self.voyage(water_distance)
self.drive(land_distance)
def __str__(self):
return f"<{self.__class__.__name__} {self.__dict__}>"
if __name__ == "__main__":
water_car = AmphibiousVehicle('4 cylinder')
print(water_car)
Let’s run this file to see our newly-configured string output.
$ python3.7 amphibious_vehicle.py
<AmphibiousVehicle {'distance_traveled': 0, 'unit': 'miles', 'boat_type': 'motor', 'tires': ['tire', 'tire'], 'engine': '4 cylinder'}>
We wouldn’t want to drop that into a message printed to end-users of our code, but this makes print debugging way more informative than seeing the location for the object in memory.
[]: