Python Classes¶
Because Python is an object-oriented programming language, you can create custom structures for storing data and methods called classes. A class represents an object and stores variables related to and functions that operate on that object. You’re already familiar with Python classes, even if you don’t know it: the Table
s you work with in Data 8 are Python classes, as are NumPy arrays.
We use classes because they allow us to store data in a rigorously structured way and provide standardized methods of accessing and interacting with that data. For example, let’s say you want to create a program that manages a person’s banking information. You need to store their name, account number, and balance. You might do something like create an array for each individual, where the first element is their name, the second is their account number, and the third is their balance:
account1 = make_array("Jane Doe", 123456, 100)
account2 = make_array("John Doe", 234567, 80)
But what happens if you need to track more data? Or suppose the structure of this data changes? Then you need to go to every place where you access an element of the array and update it! It’s really easy to forget things like this or to have instances fall through the cracks. Instead, we might create an Account
class, so that whenever we need to update the structure, we need only do so once. (This is a very simplified version of a complex topic called data abstraction that demonstrates the need for complex, templated data structures and methods of accessing their data without violating abstraction barriers.)
Some terminology: a class is the abstract definition of one such data structure, the definition from which class instances are created. When refer to an instance, we mean a single copy of one of these objects. It’s kind-of like cookies and cookie cutters: the class is the cookie cutter, the template from which we make instances, the cookies. Think about tables: Table
is the class from which we create table instances:
Table # this is the class
tbl = Table() # this is an instance
Instances are created by calling the constructor (more below) as if it were a function (e.g. Table()
).
Creating Instances¶
Classes can be created using a class
statement. Inside the statement, you put the variables and methods that define the class. The first and most important of these methods is the __init__
method which is called when an instance of a class is created. __init__
is an example of Python’s dunder (double-underscore) methods, which are used to allow classes to interact with built-in functions and operators.
The __init__
method should take any arguments needed for the class and create all of the instance variables that the instance tracks. Consider the Car
class:
class Car:
def __init__(self, make, model, year, color):
self.make = make
self.model = model
self.year = year
self.color = color
Note that the first argument to the __init__
method is a variable called self
; this argument will be filled by Python with the instance of class that is being called. For example, when we call an instance’s method (a function included in the class), we might have something like:
class Foo:
def bar(self):
return None
foo = Foo()
foo.bar()
When we run foo.bar()
, the function Foo.bar
is called and the first argument (self
) is filled with the instance foo
.
In the __init__
method (or any method, for that matter), we create instance variables (variables tied to a single instance of a class) using <instance>.<variable name>
syntax, e.g.
self.some_variable = "some value"
If we’re outside of a method, we can use the same syntax using the variable name:
foo.some_variable = "some value"
When you create a Car
, Car.__init__
is called by Python. We can create a Car
and access the values of its instance variables using the dot syntax.
car = Car("Honda", "Civic", 2018, "blue")
car.make
'Honda'
Class Representations¶
Now let’s see what our car
object (an instance of the Car
class) looks like.
car
<__main__.Car at 0x10848c358>
Hmm, that representation isn’t very descriptive. Another dunder method of Python’s is __repr__
, which defines a string representation of an object. Let’s define one for our Car
class.
class Car:
def __init__(self, make, model, year, color):
self.make = make
self.model = model
self.year = year
self.color = color
def __repr__(self):
return self.color + " " + str(self.year) + " " +self.make + " " + self.model
Now that we have defined Car.__repr__
, we can get a nicer representation of car
.
car = Car("Honda", "Civic", 2018, "blue")
car
blue 2018 Honda Civic
Operators¶
Now let’s create two of the same cars and compare them. They should be equal, right…?
car_1 = Car("Honda", "Civic", 2018, "blue")
car_2 = Car("Honda", "Civic", 2018, "blue")
car_1 == car_2
False
They aren’t equal! That’s because, by default, the custom classes are only equal if they are the same instance, so car_1 == car_1
is True
but car_1 == car_2
is False
. For this reason, we need to define the __eq__
dunder method of Car
which Python will call when we use the ==
operator on a Car
. We’ll say and object is equal to a Car
if the other object is also a Car
(determined using the isinstance
function) and has the same four attributes as the current car.
class Car:
def __init__(self, make, model, year, color):
self.make = make
self.model = model
self.year = year
self.color = color
def __repr__(self):
return f"{self.color} {self.year} {self.make} {self.model}"
def __eq__(self, other):
if isinstance(other, Car):
return self.make == other.make and self.model == other.model and \
self.year == other.year and self.color == other.color
return False
Now our call from above will work:
car_1 = Car("Honda", "Civic", 2018, "blue")
car_2 = Car("Honda", "Civic", 2018, "blue")
car_1 == car_2
True
Other important dunder methods include
Method Name |
Description |
---|---|
|
the string representation of an object |
|
length of an object ( |
|
less than, greater than, less than or equal to, and greater than or equal to operators, resp. |
|
hash function value ( |
|
getter and setter (resp.) for indexes ( |
|
getter and setter (resp.) for dot syntax ( |
Note that when using comparison operators the object to the left of the operator has its comparison operator method called. In the below example, the first comparison calls point_1.__lt__
and the second calls point_2.__lt__
.
point_1 = Point()
point_2 = Point()
point_1 < point_2 # calls point_1.__lt__
point_2 < point_1 # calls point_2.__lt__
Instance Methods¶
Now let’s define some methods for a Car
. We’ll add a few more instance variables:
car.mileage
is the number of miles driven by the carcar.gas
is number of gallons of gas in the tankcar.mpg
is the number of miles to a gallon that the car gets.
Note that car.mileage
and car.gas
are initialized to 0 when we create the car in __init__
. We’ll first define the fill_tank
method, which fills the gas tank to 10 gallons.
class Car:
def __init__(self, make, model, year, color, mpg):
self.make = make
self.model = model
self.year = year
self.color = color
self.mpg = mpg
self.mileage = 0
self.gas = 0
def __repr__(self):
return f"{self.color} {self.year} {self.make} {self.model}"
def __eq__(self, other):
if isinstance(other, Car):
return self.make == other.make and self.model == other.model and \
self.year == other.year and self.color == other.color
return False
def fill_tank(self):
self.gas = 10
We can create a car and fill its take by calling car.fill_tank
.
car = Car("Honda", "Civic", 2018, "blue", 18)
car.fill_tank()
car.gas
10
Assertions¶
Now we’ll define the car.drive
method that drives miles
miles and ensures that we have enough gas to drive that far by throwing an AssertionError
if we don’t.
We throw assertion errors using an assert
statement which takes two arguments: a boolean expression and a string telling the user what caused the error. For example, if we want to make sure that a string has no spaces, we might write
assert " " not in string, "Spaces found in string"
Then, if string
has a space, the user would see:
string = "some string"
assert " " not in string, "Spaces found in string"
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-20-18b8ef0eb9ac> in <module>()
1 string = "some string"
----> 2 assert " " not in string, "Spaces found in string"
AssertionError: Spaces found in string
Reassignment Operators¶
Another new syntax needed for the Car.drive
method is +=
and -=
. An operator followed by =
tells Python to perform the operation combining the values on the left and right sides of the operator and then reassigns this value to the variable on the left side. This means that the expression x += 2
is the exact same as x = x + 2
.
x = 2
x += 1
print(x) # x is now 3
x -= 4
print(x) # x is now -1
x *= 100
print(x) # x is now -100
x /= -100
print(x) # x is now 1
3
-1
-100
1.0
Now let’s define Car.drive
.
class Car:
def __init__(self, make, model, year, color, mpg):
self.make = make
self.model = model
self.year = year
self.color = color
self.mpg = mpg
self.mileage = 0
self.gas = 0
def __repr__(self):
return f"{self.color} {self.year} {self.make} {self.model}"
def __eq__(self, other):
if isinstance(other, Car):
return self.make == other.make and self.model == other.model and \
self.year == other.year and self.color == other.color
return False
def fill_tank(self):
self.gas = 10
def drive(self, miles):
assert miles <= self.gas * self.mpg, f"not enough gas to drive {self.miles} miles"
self.mileage += miles
self.gas -= miles / self.mpg
Let’s drive our Car
100 miles.
car = Car("Honda", "Civic", 2018, "blue", 18)
car.fill_tank()
car.drive(100)
car.mileage, car.gas
(100, 4.444444444444445)
Now let’s see how many miles we have left to drive by defining Car.miles_to_empty
.
class Car:
def __init__(self, make, model, year, color, mpg):
self.make = make
self.model = model
self.year = year
self.color = color
self.mpg = mpg
self.mileage = 0
self.gas = 0
def __repr__(self):
return f"{self.color} {self.year} {self.make} {self.model}"
def __eq__(self, other):
if isinstance(other, Car):
return self.make == other.make and self.model == other.model and \
self.year == other.year and self.color == other.color
return False
def fill_tank(self):
self.gas = 10
def drive(self, miles):
assert miles <= self.gas * self.mpg, f"not enough gas to drive {self.mileage} miles"
self.mileage += miles
self.gas -= miles / self.mpg
def miles_to_empty(self):
return self.gas * self.mpg
car = Car("Honda", "Civic", 2018, "blue", 18)
car.fill_tank()
car.drive(100)
car.miles_to_empty()
80.0
We have 80 miles left before we’re empty, so we see that if we try to drive 90 miles, the car will thrown an error:
car.drive(90)
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-25-8ae22d6f2de2> in <module>()
----> 1 car.drive(90)
<ipython-input-24-e4c645b85ba9> in drive(self, miles)
21
22 def drive(self, miles):
---> 23 assert miles <= self.gas * self.mpg, f"not enough gas to drive {self.mileage} miles"
24 self.mileage += miles
25 self.gas -= miles / self.mpg
AssertionError: not enough gas to drive 100 miles
For more information on Python classes, check out Sections 2.5-2.7 of Composing Programs, the CS 61A/CS 88 textbook.