class
es and self
Let’s practice making a simple class. Open a new file and save it as class_example.py
, we’ll be running this from the command line rather than the REPL. Pass in several variables and save them to the instance by using self
:
# class_example.py
class Vehicle:
def __init__(self, make, model, fuel="gas"):
self.make = make
self.model = model
self.fuel = fuel
daily_driver = Vehicle("Subaru", "Crosstrek")
# By default, this is how python represents our object:
print(daily_driver)
# The variables we saved to the instance are available like this:
print(f"I drive a {daily_driver.make} {daily_driver.model}. It runs on {daily_driver.fuel}.")
class
VariablesWe can also add class variables - variables that exist for all instances of a class. Let’s add a variable called number_of_wheels
to the class scope:
# class_example.py
class Vehicle:
number_of_wheels = 4
def __init__(self, make, model, fuel="gas"):
self.make = make
self.model = model
self.fuel = fuel
Let’s query both the instance and class variables. Note that we set the instance variable to 3, but the higher-level class variable is still set to 4.
# class_example.py
daily_driver = Vehicle("Subaru", "Crosstrek")
daily_driver.number_of_wheels = 3
# Instance variables
print(f"I drive a {daily_driver.make} {daily_driver.model}. It runs on {daily_driver.fuel}.")
print(f"My {daily_driver.model} has {daily_driver.number_of_wheels} wheels.")
# Class variable
print(f"Most vehicles have {Vehicle.number_of_wheels} wheels.")
Class inheritance in Python is super useful - you can easily create a hierarchy of classes to make your life easier and maximize code reuse. Let’s subclass our Vehicle
class and extend it by breaking out Cars and Trucks.
# class_example.py
class Vehicle:
def __init__(self, make, model, fuel="gas"):
self.make = make
self.model = model
self.fuel = fuel
class Car(Vehicle):
number_of_wheels = 4
class Truck(Vehicle):
number_of_wheels = 6
def __init__(self, make, model, fuel="diesel"):
super().__init__(make, model, fuel)
Note: we’ve moved the number_of_wheels
variable to the subclasses. Our Car
subclass sets this variable but instantiating a Car
just passes through to Vehicle.__init__()
. We do, however, provide a __init__()
for Truck
, which changes the default fuel
to diesel
and then calls super().__init__()
which redirects to Vehicle.__init__()
. This lets us make changes that are specific to Truck
instances (but we can still call them Vehicles
). Let’s instantiate our subclasses:
# class_example.py
daily_driver = Car("Subaru", "Crosstrek")
print(f"I drive a {daily_driver.make} {daily_driver.model}. "
f"It uses {daily_driver.fuel} and has {daily_driver.number_of_wheels} wheels.")
truck = Truck("Ford", "F350")
print(f"I also have a {truck.make} {truck.model}. "
f"It uses {truck.fuel} and has {truck.number_of_wheels} wheels.")
type
, isinstance
, and issubclass
The type()
command tells us the type of an object - for example, a Truck
or a Car
. Note that it doesn’t know anything about inheritance, so you can’t use type()
to check if a Car
is a Vehicle
. For that, we can use isinstance()
. issubclass()
is another useful function that we can use to see if a class (rather than an instance) is a subclass of another class. Add this to your code:
# class_example.py
print(f"My daily driver is a {type(daily_driver)} and my truck is a {type(truck)}")
print(f"Is my daily driver a car? {isinstance(daily_driver, Car)}")
print(f"Is my truck a Vehicle? {isinstance(truck, Vehicle)}")
print(f"Is my truck a Car? {isinstance(truck, Car)}")
print(f"Is a Truck a subclass of Vehicle? {issubclass(Truck, Vehicle)}")
Let’s get more comfortable with exceptions. First, you’ve probably seen this one already: The IndentationError
.
>>> def my_function():
... print("Hello!")
File "<stdin>", line 2
print("Hello!")
^
IndentationError: expected an indented block
Notice that we started a new function scope with the def
keyword, but didn’t indent the next line of the function, the print()
argument.
You’ve probably also seen the more general SyntaxError
. This one’s probably obvious - something is misspelled, or the syntax is otherwise wrong. Python gives us a helpful little caret ^
under the earliest point where the error was detected, however you’ll have to learn to read this with a critical eye as sometimes the actual mistake precedes the invalid syntax. For example:
>>> a = [4,
... x = 5
File "<stdin>", line 2
x = 5
^
SyntaxError: invalid syntax
Here, the invalid syntax is x = 5
, because assignment statements aren’t valid list elements, however the actual error is the missing right bracket ]
on the line above.
You’ll get plenty of practice triggering syntax errors on your own. Let’s practice triggering some exceptions. Type this perfectly valid code into your REPL and see what happens:
>>> a = 1 / 0
Of course, you’ll get a divide-by-zero error, or as Python calls it, ZeroDivisionError
. Some other common errors are TypeError
when trying to perform an action on two unrelated types, KeyError
when trying to access a dictionary key that doesn’t exist, and AttributeError
when trying to access a variable or call a function that doesn’t exist on an object.
>>> 2 + "3"
>>> my_dict = {"hello": "world"}
>>> my_dict["foo"]
>>> my_dict.append("foo")
Making our own Exceptions is cheap and easy, and useful for keeping track of various error states that are specific to your application. Simply inherit from the general Exception
class:
>>> class MyException(Exception):
... pass
...
>>> raise MyException()
It’s also sometimes helpful to change the default behavior for your custom Exceptions. In this case, you can simply provide your own __init__()
method inside your Exception subclass:
>>> class MyException(Exception):
... def __init__(self, message):
... new_message = f"!!!ERROR!!! {message}"
... super().__init__(new_message)
...
>>> raise MyException("Something went wrong!")
try
, except
In Python, the “try-catch” statements use try
and except
. As we discussed, try
is the code that could possibly throw an Exception, and except
is the code that runs if the error is raised. Practice catching a KeyError
by try
ing to access a fake dictionary key:
>>> try:
... my_dict = {"hello": "world"}
... print(my_dict["foo"])
... except KeyError:
... print("Oh no! That key doesn't exist")
...
Let’s add in catching the specific KeyError
object so that we can access it during the except
block:
>>> try:
... my_dict = {"hello": "world"}
... print(my_dict["foo"])
... except KeyError as key_error:
... print(f"Oh no! The key {key_error} doesn't exist!")
...
Sometimes it’s helpful to catch an error, perform an action, and then pass the error on rather than swallowing it. This is useful when, for example, something goes wrong deep inside your code and you need to perform a special action, but also let code further up the chain know that something is wrong and the program can’t continue. Let’s divide one number by other, decrementing until we hit zero. Catch that error and immediately raise a RuntimeError
:
>>> while True:
... for divisor in range(5, -1, -1):
... try:
... quotient = 10 / divisor
... print(f"10 / {divisor} = {quotient}")
... except ZeroDivisionError:
... print("Oops! We tried to divide by zero!")
... raise RuntimeError
...
class
es and self
class
Variablestype
, isinstance
, and issubclass
try
, except