Classes#
Classes are the fundamental concept for object oriented programming. A class defines a data type with both data and functions that can operate on the data. An object is an instance of a class. Each object will have its own namespace (separate from other instances of the class and other functions, etc. in your program).
We use the dot operator, .
, to access members of the class (data or functions). We’ve already been doing this a lot, strings, ints, lists, … are all objects in python.
Naming conventions#
The python community has some naming convections, defined in PEP-8:
https://www.python.org/dev/peps/pep-0008/
The widely adopted ones are:
class names start with an uppercase, and use “camelcase” for multiword names, e.g.
ShoppingCart
variable names (including objects which are instances of a class) are lowercase and use underscores to separate words, e.g.,
shopping_cart
module names should be lowercase with underscores
A simple class#
Here’s a class that holds some student info
class Student:
def __init__(self, name, grade=None):
self.name = name
self.grade = grade
This has a function, __init__()
which is called automatically when we create an instance of the class.
The argument self
refers to the object that we will create, and points to the memory that they object will use to store the class’s contents.
a = Student("Mike")
print(a.name)
print(a.grade)
Mike
None
Let’s create a bunch of them, stored in a list
students = []
students.append(Student("fry", "F-"))
students.append(Student("leela", "A"))
students.append(Student("zoidberg", "F"))
students.append(Student("hubert", "C+"))
students.append(Student("bender", "B"))
students.append(Student("calculon", "C"))
students.append(Student("amy", "A"))
students.append(Student("hermes", "A"))
students.append(Student("scruffy", "D"))
students.append(Student("flexo", "F"))
students.append(Student("morbo", "D"))
students.append(Student("hypnotoad", "A+"))
students.append(Student("zapp", "Q"))
Quick Exercise
Loop over the students in the students
list and print out the name and grade of each student, one per line.
We can use list comprehensions with our list of objects. For example, let’s find all the students who have A’s
As = [q.name for q in students if q.grade.startswith("A")]
As
['leela', 'amy', 'hermes', 'hypnotoad']
Playing Cards#
Here’s a more complicated class that represents a playing card. Notice that we are using unicode to represent the suits.
class Card:
def __init__(self, suit=1, rank=2):
if suit < 1 or suit > 4:
print("invalid suit, setting to 1")
suit = 1
self.suit = suit
self.rank = rank
def value(self):
""" we want things order primarily by rank then suit """
return self.suit + (self.rank-1)*14
# we include this to allow for comparisons with < and > between cards
def __lt__(self, other):
return self.value() < other.value()
def __eq__(self, other):
return self.rank == other.rank and self.suit == other.suit
def __repr__(self):
return self.__str__()
def __str__(self):
suits = [u"\u2660", # spade
u"\u2665", # heart
u"\u2666", # diamond
u"\u2663"] # club
r = str(self.rank)
if self.rank == 11:
r = "J"
elif self.rank == 12:
r = "Q"
elif self.rank == 13:
r = "K"
elif self.rank == 14:
r = "A"
return r +':'+suits[self.suit-1]
we can create a card easily.
c1 = Card()
We can pass arguments to __init__
in when we setup the class:
c2 = Card(suit=2, rank=2)
Once we have our object, we can access any of the functions in the class using the dot
operator
c2.value()
16
c3 = Card(suit=0, rank=4)
invalid suit, setting to 1
The __str__
method converts the object into a string that can be printed.
print(c1)
print(c2)
2:♠
2:♥
the value method assigns a value to the object that can be used in comparisons, and the __lt__
method is what does the actual comparing
print(c1 > c2)
print(c1 < c2)
False
True
Note that not every operator is defined for our class, so, for instance, we cannot add two cards together:
c1 + c2
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[12], line 1
----> 1 c1 + c2
TypeError: unsupported operand type(s) for +: 'Card' and 'Card'
Quick Exercise
Create a “hand” corresponding to a straight (5 cards of any suite, but in sequence of rank)
Create another hand corresponding to a flush (5 cards all of the same suit, of any rank)
Finally create a hand with one of the cards duplicated—this should not be allowed in a standard deck of cards. How would you check for this?
Operators#
We can define operations like +
and -
that work on our objects. Here’s a simple example of currency—we keep track of the country and the amount
class Currency:
""" a simple class to hold foreign currency """
def __init__(self, amount, country="US"):
self.amount = amount
self.country = country
def __add__(self, other):
return Currency(self.amount + other.amount, country=self.country)
def __sub__(self, other):
return Currency(self.amount - other.amount, country=self.country)
def __str__(self):
return f"{self.amount} {self.country}"
We can now create some monetary amounts for different countries
d1 = Currency(10, "US")
d2 = Currency(15, "US")
print(d2 - d1)
5 US
Quick Exercise
As written, our Currency class has a bug—it does not check whether the amounts are in the same country before adding. Modify the __add__
method to first check if the countries are the same. If they are, return the new Currency
object with the sum, otherwise, return None
.
Vectors Example#
Here we write a class to represent 2-d vectors. Vectors have a direction and a magnitude. We can represent them as a pair of numbers, representing the x and y lengths. We’ll use a tuple internally for this
We want our class to do all the basic operations we do with vectors: add them, multiply by a scalar, cross product, dot product, return the magnitude, etc.
We’ll use the math module to provide some basic functions we might need (like sqrt)
This example will show us how to overload the standard operations in python. Here’s a list of the builtin methods:
https://docs.python.org/3/reference/datamodel.html
To make it really clear what’s being called when, I’ve added prints in each of the functions
import math
class Vector:
""" a general two-dimensional vector """
def __init__(self, x, y):
print("in __init__")
self.x = x
self.y = y
def __str__(self):
print("in __str__")
return f"({self.x} î + {self.y} ĵ)"
def __repr__(self):
print("in __repr__")
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
print("in __add__")
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
else:
# it doesn't make sense to add anything but two vectors
print(f"we don't know how to add a {type(other)} to a Vector")
raise NotImplementedError
def __sub__(self, other):
print("in __sub__")
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
else:
# it doesn't make sense to add anything but two vectors
print(f"we don't know how to add a {type(other)} to a Vector")
raise NotImplementedError
def __mul__(self, other):
print("in __mul__")
if isinstance(other, int) or isinstance(other, float):
# scalar multiplication changes the magnitude
return Vector(other*self.x, other*self.y)
else:
print("we don't know how to multiply two Vectors")
raise NotImplementedError
def __matmul__(self, other):
print("in __matmul__")
# a dot product
if isinstance(other, Vector):
return self.x*other.x + self.y*other.y
else:
print("matrix multiplication not defined")
raise NotImplementedError
def __rmul__(self, other):
print("in __rmul__")
return self.__mul__(other)
def __truediv__(self, other):
print("in __truediv__")
# we only know how to multiply by a scalar
if isinstance(other, int) or isinstance(other, float):
return Vector(self.x/other, self.y/other)
def __abs__(self):
print("in __abs__")
return math.sqrt(self.x**2 + self.y**2)
def __neg__(self):
print("in __neg__")
return Vector(-self.x, -self.y)
def cross(self, other):
# a vector cross product -- we return the magnitude, since it will
# be in the z-direction, but we are only 2-d
return abs(self.x*other.y - self.y*other.x)
This is a basic class that provides two methods __str__
and __repr__
to show a representation of it. There was some discussion of this on slack. These two functions provide a readable version of our object.
The convection is what __str__
is human readable while __repr__
should be a form that can be used to recreate the object (e.g., via eval()
). See:
http://stackoverflow.com/questions/1436703/difference-between-str-and-repr-in-python
v = Vector(1,2)
v
in __init__
in __repr__
Vector(1, 2)
print(v)
in __str__
(1 î + 2 ĵ)
Vectors have a length, and we’ll use the abs()
builtin to provide the magnitude. For a vector:
we have
abs(v)
in __abs__
2.23606797749979
Let’s look at mathematical operations on vectors now. We want to be able to add and subtract two vectors as well as multiply and divide by a scalar.
u = Vector(3,5)
in __init__
w = u + v
print(w)
in __add__
in __init__
in __str__
(4 î + 7 ĵ)
u - v
in __sub__
in __init__
in __repr__
Vector(2, 3)
It doesn’t make sense to add a scalar to a vector, so we didn’t implement this – what happens?
u + 2.0
in __add__
we don't know how to add a <class 'float'> to a Vector
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
Cell In[23], line 1
----> 1 u + 2.0
Cell In[16], line 24, in Vector.__add__(self, other)
21 else:
22 # it doesn't make sense to add anything but two vectors
23 print(f"we don't know how to add a {type(other)} to a Vector")
---> 24 raise NotImplementedError
NotImplementedError:
Now multiplication. It makes sense to multiply by a scalar, but there are multiple ways to define multiplication of two vectors.
Note that python provides both a __mul__
and a __rmul__
function to define what happens when we multiply a vector by a quantity and what happens when we multiply something else by a vector.
u*2.0
in __mul__
in __init__
in __repr__
Vector(6.0, 10.0)
2.0*u
in __rmul__
in __mul__
in __init__
in __repr__
Vector(6.0, 10.0)
and division: __truediv__
is the python 3 way of division /
, while __floordiv__
is the old python 2 way, also enabled via //
.
Dividing a scalar by a vector doesn’t make sense:
u/5.0
in __truediv__
in __init__
in __repr__
Vector(0.6, 1.0)
5.0/u
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[27], line 1
----> 1 5.0/u
TypeError: unsupported operand type(s) for /: 'float' and 'Vector'
Python 3.5 introduced a new matrix multiplication operator, @
– we’ll use this to implement a dot product between two vectors:
u @ v
in __matmul__
13
For a cross product, we don’t have an obvious operator, so we’ll use a function. For 2-d vectors, this will result in a scalar
u.cross(v)
1
Finally, negation is a separate operation:
-u
in __neg__
in __init__
in __repr__
Vector(-3, -5)