Classes in Python:

Object oriented programming languages like Python are powerfull because they allow you create new object types, modifiy object properites and manipulate objects with methods. An object can be resused and built upon to create new objects. So what is an "object" anyway? In Python, every instance of a class is an object.

Lets take a look at an imaginary example. Pretend your grandmother Helen is created in Python. To create a new grandma we "instantiate" or create a new object (Helen) of a class(grandmas). In this case grandma is the name of the class, and Helen is a specific instance of that class. Helen is a grandma, but there are a bunch of other grandmas too. Every property that a grandma can have, our grandma Helen can have. All of the opperations a grandma can do, our grandma Helen can do too. We need to type encolsed parenthesis () after the class name grandma in order to tell Python that we want Helen to be made from the grandma class (and not just create a variable helen and assign it whatever value is stored in the variable grandma):

# Creat a new Object Helen, a member of the grandma Class
Helen = grandma()

Once our awesome grandma Helen has been created, we could imagine our Grandma's attributes like her address. A specific attribute of a class is assigned using the dot . notation

Helen.address = 'Portland, OR'

Maybe our grandma has a couple children, like our mom Jan and our three aunts, Ellie, Manya and Cookie.

Helen.children = ['Jan','Ellie','Manya','Cookie']

And our grandma as some grandchildren too, like me, my brother Zach and my sister Liza.

Helen.grandchildren = ['me','Zach','Liza']

Once we have assigned these properties to our grandma, we can access them using the dot notation as well.

>> Helen.address
'Portland, OR'
>> Helen.grandchildren
['me','Zach','Liza']

We can even imagine doing opperations on our grandma (not gall blatter surgury, arithmetic opperations). Suppose we create two grandmas, Helen and Margret and they each have three grandchildren:

>> Helen = grandma()
>> Helen.grandchildren = ['me','Zach','Liza']
>> Margret = grandma()
>> Margret.grandchildren = ['Sophie','Josh','Seth']

If we add our two grandmas together, we get a super grandma! In Python, we can assign specific ways that arithmetic opperations (like +, -, x, / ) are run on our grandma class. Imagine that when we add two grandmas together we get a super grandma who has all of the grandchildren of both grandmas combined:

>> SuperGrandma = Helen + Margret
>> SuperGrandma.grandchildren
['me','Zach', 'Liza', 'Sophie', 'Josh', 'Seth']

Create a new class in Python

So let's create a new Grandma class in Python. In Python you create new classes by using the class keyword followed by the class name and a colon :. Class names are usually capitalized, so our first line will be:

class Grandma:

Then we need to indent the set of instructions which detail the properites of our Grandma class. All the definitions need to be indented. First we will define a special method __init___. This special method is what happens when we create a new instance of a class. It __init__ method is what happens when we call Helen = Grandma(). It is the method that runs when a new object of a class is created.


In [1]:
class Grandma:
    
    def __init__(self):
        self.address = ' '
        self.children = []
        self.grandchildren = []

Now we can create a new instance of the Grandma class, our grandma Helen


In [2]:
Helen = Grandma()

Let's see if Helen is part of our Grandma class using Python's type() function.


In [3]:
type(Helen)


Out[3]:
__main__.Grandma

Now we can define our grandma's attributes. These are attributes that all grandmas have. Our Grandma has Helen different values for these attributes than other grandmas. All grandmas have grandchildren (same atribute), but each grandma has different grandchildren (different values).


In [3]:
Helen.address ='Portland, OR'
Helen.children = ['Jan','Holly','Manya','Cookie']
Helen.grandchildren = ['me', 'Zach','Liza']

Now let's access these attributes using the dot . notation. The general syntax is Object.property


In [4]:
Helen.address


Out[4]:
'Portland, OR'

In [5]:
Helen.children


Out[5]:
['Jan', 'Holly', 'Manya', 'Cookie']

In [6]:
Helen.grandchildren


Out[6]:
['me', 'Zach', 'Liza']

Next let's figure out a way to add new grandchildren. Imagine we have a new baby cousin who's name is Gabby. Let's create a method for our Grandma class which adds another name in the list of grandchildren.


In [8]:
class Grandma:
    
    def __init__(self):
        self.address = ' '
        self.children = []
        self.grandchildren = []
        
    def add_grandchild(self,new_grandchild):
        current_grandchildren = self.grandchildren
        self.granchildren = current_grandchildren.append(new_grandchild)
        return self

In [9]:
Helen = Grandma()
Helen.address ='Portland, OR'
Helen.children = ['Jan','Holly','Manya','Cookie']
Helen.grandchildren = ['me', 'Zach','Liza']

In [10]:
Helen.add_grandchild('Gabby')
Helen.grandchildren


Out[10]:
['me', 'Zach', 'Liza', 'Gabby']

Let's now add one more method to our Grandma class. Let's write a method that produces a super grandma when two grandmas are added together. To do this we need to define the __add___ method. The ___add___ method runs when we use the plus sign + used, as in:

solution = 2 + 2

In the case of our Grandma class, we want to combine the grandchildren of both grandmas to create a new super grandma

supergrandma = Helen + Margret

We do this by defining the __add__ method within our Grandma class


In [11]:
class Grandma:
    
    def __init__(self): # what happens when you create a new Grandma
        self.address = ' '
        self.children = []
        self.grandchildren = []
        
    def add_grandchild(self,new_grandchild):
        current_grandchildren = self.grandchildren
        self.granchildren = current_grandchildren.append(new_grandchild)
        return self
    
    def __add__(self,other_grandma): # what happens when you + two Grandma's
        super_gran = Grandma()
        super_gran.grandchildren = self.grandchildren + other_grandma.grandchildren
        return super_gran

In [12]:
Helen = Grandma()
Margret = Grandma()
Helen.grandchildren = ['me', 'Zach','Liza']
Margret.grandchildren = ['Nichole','Mikka','Ari']

In [13]:
SuperGran = Helen + Margret
SuperGran.grandchildren


Out[13]:
['me', 'Zach', 'Liza', 'Nichole', 'Mikka', 'Ari']

A Vector class

I love grandma's, especially Helen and Margret, but how can we use Python classes in engineering? One type of class that would be especiall useful would be a Vector class. Our Vector class would have a couple properties: A magnitude, an x-component, a y-compenent, and a z-component. Also some Vectors are unit vectors, which have a magnitude of 1. Other Vectors are not unit vectors and have a magnitude that is anything else besides 1. It would be nice to have a method to quickly figure out whether a given vector is a unit vector or not.

To create our new Vector class. We need to use the class keyword followed by the name of our class, Vector and a colon :

class Vector:

The first method we need to define is the special __init__ method that runs when a new Vector is created. When we create a new Vector, we need to define the i, j and k components. We'll make each of these components a different property of each Vector.


In [14]:
class Vector:
    
    def __init__(self, i, j, k):
        self.i = i
        self.j = j
        self.k = k

In [15]:
F = Vector(3,4,5)
F.i, F.j, F.k


Out[15]:
(3, 4, 5)

I'd like each of the components be a floating point number rather than an integer. We can ensure this as part of our __init__ method. It would also be good to be able to create a vector with two components, just i and j, in case we are working in two dimmesions. To do this we will set the k value to zero by default. If the user doen't list a k value when they create a new vector, the k value will be set to zero.


In [16]:
class Vector:
    
    def __init__(self, i, j, k=0.0):
        self.i = float(i)
        self.j = float(j)
        self.k = float(k)

In [17]:
F = Vector(3,4)
F.i, F.j, F.k


Out[17]:
(3.0, 4.0, 0.0)

Each Vector should also have a magnitude. The magnitude of a vector is the square root of the sum of the squares of the vector's components.

$$ \left| \vec{F} \right| = \sqrt{F_x^2 + F_y^2 + F_z^2} $$

Remember that to make exponents in Python, you need to use the double asterix ** not the carrot ^ symbol. We also need to import the sqrt() function from Python's math module. sqrt() is part of the standard library of Python functions, but it still needs to be imported.


In [18]:
from math import sqrt

class Vector:
    
    def __init__(self, i, j, k=0.0):
        self.i = float(i)
        self.j = float(j)
        self.k = float(k)
        self.mag = sqrt(self.i**2 + self.j**2 + self.k**2)

In [19]:
F = Vector(3,4)
F.mag


Out[19]:
5.0

Now let's make a method that will tell us wheather our Vector is a unit vector or not. If our vector is a unit vector, running the method on our vector object will output true, If our vector is not a unit vector, running the method on our vector object will output false. Our vector object is considered a unit vector if it has a magnitude = 1. Because our vector magnitude is going to be a floating point number, we need to be careful with comparison statements. If there is just a little floating point arithmetic error, then the statment mag == 1 may not return true, even if mathmatically the magnitude should be 1. Because of this we will use an upper and lower bound of about 12 significant figures. If the magnitude is within 12 significant figures of 1 we'll call it 1.


In [20]:
from math import sqrt

class Vector:
    
    def __init__(self, i, j, k=0.0):
        self.i = float(i)
        self.j = float(j)
        self.k = float(k)
        self.mag = sqrt(self.i**2 + self.j**2 + self.k**2)
        
    def is_unit_vector(self):
        if self.mag < 1.00000000001 and self.mag > 0.999999999999:
            return True
        else:
            return False

In [21]:
from math import sqrt, sin, cos, pi
F = Vector(cos(pi/4),sin(pi/4))
print(F.mag)
F.is_unit_vector()


1.0
Out[21]:
True

Vector Algebra

Great! What are some other things we would like to be able to do with our Vectors? Adding and subtracting vectors is one of them. To add and vectors, we just sum up the components of each. To subtract vectors, we subtract the components of one from the components of the other.

$$ \vec{P} + \vec{Q} = ( P_x + Q_x )\hat{i} + ( P_y + Q_y )\hat{j} + ( P_z + Q_z )\hat{k} $$$$ \vec{P} - \vec{Q} = ( P_x - Q_x )\hat{i} + ( P_y - Q_y )\hat{j} + ( P_z - Q_z )\hat{k} $$

To be able to do this type of Vector addition (use the + symbol) with our Vector objects, we need to modify the __add__ method. This will allow us to access the functionality of the plus + sign.

To use the subtraction sign ( - ) with our Vector objects we need to modify the __sub__ method. This will allow us to access the functionality of the minus - sign.


In [22]:
from math import sqrt

class Vector:
    
    def __init__(self, i, j, k=0.0):
        self.i = float(i)
        self.j = float(j)
        self.k = float(k)
        self.mag = sqrt(self.i**2 + self.j**2 + self.k**2)
        
    def is_unit_vector(self):
        if self.mag < 1.00000000001 and self.mag > 0.999999999999:
            return True
        else:
            return False
    
    def __add__(self, other): # Vector subtraction: what happens when self + other?
        return Vector(self.i + other.i, self.j + other.j, self.k + other.k)
    
    def __sub__(self, other): # Vector addition: what happens when self - other?
        return Vector(self.i - other.i, self.j - other.j, self.k - other.k)

In [23]:
P = Vector(3,4,5)
Q = Vector(1,2,3)
R = P + Q
R.i, R.j, R.k


Out[23]:
(4.0, 6.0, 8.0)

Now let's create the ability for our Vectors to be multiplied by scalars and divided by scalars. We want to have access to the times symbol * and the division symbol /. These are defined by the mul and truediv methods.


In [24]:
from math import sqrt

class Vector:
    
    def __init__(self, i, j, k=0.0):
        self.i = float(i)
        self.j = float(j)
        self.k = float(k)
        self.mag = sqrt(self.i**2 + self.j**2 + self.k**2)
        
    def is_unit_vector(self):
        if self.mag < 1.00000000001 and self.mag > 0.999999999999:
            return True
        else:
            return False
    
    def __add__(self, other): # Vector subtraction: what happens when self + other?
        return Vector(self.i + other.i, self.j + other.j, self.k + other.k)
    
    def __sub__(self, other): # Vector addition: what happens when self - other?
        return Vector(self.i - other.i, self.j - other.j, self.k - other.k)
   
    def __mul__(self, other): # Scalar multiplication: what happens when self * other?
        return Vector(self.i*other, self.j*other, self.k*other)
    
    def __truediv__(self, other): # Scalar division: what happens when self / other?
        return Vector(self.i/other, self.j/other, self.k/other)

In [25]:
F = Vector(2,3,4)
G = F*3
G.i,G.j,G.k


Out[25]:
(6.0, 9.0, 12.0)

In [26]:
F = Vector(2,3,4)
G = F/2
G.i, G.j, G.k


Out[26]:
(1.0, 1.5, 2.0)

Now let's add something special to our class so that when we type just a Vector object into the Python Interpreter, or directly into a code cell in a Jupyter Notebook, the output actually tells us something. Right now, the output isn't very useful. Printing out the Vector doesn't help. We still just get the output that our variable is part of the Vector class.


In [27]:
F = Vector(2,3,4)
F


Out[27]:
<__main__.Vector at 0x2199b95e630>

In [28]:
F = Vector(2,3,4)
print(F)


<__main__.Vector object at 0x000002199B965358>

To change this behavior we need to modifiy the __str__ and ___repr__ methods. Let's define each of them to output the vector is i,j,k form:


In [29]:
from math import sqrt

class Vector:
    
    def __init__(self, i, j, k=0.0):
        self.i = float(i)
        self.j = float(j)
        self.k = float(k)
        self.mag = sqrt(self.i**2 + self.j**2 + self.k**2)
        
    def is_unit_vector(self):
        if self.mag < 1.00000000001 and self.mag > 0.999999999999:
            return True
        else:
            return False
    
    def __add__(self, other): # Vector subtraction: what happens when self + other?
        return Vector(self.i + other.i, self.j + other.j, self.k + other.k)
    
    def __sub__(self, other): # Vector addition: what happens when self - other?
        return Vector(self.i - other.i, self.j - other.j, self.k - other.k)
   
    def __mul__(self, other): # Scalar multiplication: what happens when self * other?
        return Vector(self.i*other, self.j*other, self.k*other)
    
    def __truediv__(self, other): # Scalar division: what happens when self / other?
        return Vector(self.i/other, self.j/other, self.k/other)
    
    def __str__(self):
        return('{}i + {}j + {}k'.format(self.i, self.j, self.k))
    
    def __repr__(self):
        return('{}i + {}j + {}k'.format(self.i ,self.j, self.k))

In [30]:
F = Vector(2,3,4)
print(F)
F


2.0i + 3.0j + 4.0k
Out[30]:
2.0i + 3.0j + 4.0k

Now we are going to modify our Vector class so that we can iterate through the different term, just like you can iterate through a list. We want to be able to do an opperation like:

for comp in F:
    print(comp)

Right now if we try that, we are returned an error:


In [31]:
F = Vector(2,3,4)
for comp in F:
    print(F)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-31-a3d25a53d45f> in <module>()
      1 F = Vector(2,3,4)
----> 2 for comp in F:
      3     print(F)

TypeError: 'Vector' object is not iterable

To make our Vector class iterable, we need to define two methods, the __next___ method and the __iter__ method. The __iter__ method will simply return back the Vector object, but the __next__ method has to tell the iterator which property to access next. We are just going to cycle through the i, j, and k component properites and not include the magnitudue. To do this, we need to insert a property counter when the Vector object is created that is set to zero. This will allow the __next__ method to have a counter to cycle through.


In [32]:
from math import sqrt

class Vector:
    
    def __init__(self, i, j, k=0.0):
        self.i = float(i)
        self.j = float(j)
        self.k = float(k)
        self.mag = sqrt(self.i**2 + self.j**2 + self.k**2)
        self.term = 0
        
    def is_unit_vector(self):
        if self.mag < 1.00000000001 and self.mag > 0.999999999999:
            return True
        else:
            return False
    
    def __add__(self, other): # Vector subtraction: what happens when self + other?
        return Vector(self.i + other.i, self.j + other.j, self.k + other.k)
    
    def __sub__(self, other): # Vector addition: what happens when self - other?
        return Vector(self.i - other.i, self.j - other.j, self.k - other.k)
   
    def __mul__(self, other): # Scalar multiplication: what happens when self * other?
        return Vector(self.i*other, self.j*other, self.k*other)
    
    def __truediv__(self, other): # Scalar division: what happens when self / other?
        return Vector(self.i/other, self.j/other, self.k/other)
    
    def __str__(self):
        return('{}i + {}j + {}k'.format(self.i, self.j, self.k))
    
    def __repr__(self):
        return('{}i + {}j + {}k'.format(self.i ,self.j, self.k))
    
    def __next__(self):
        if self.term >= 3:
            raise StopIteration
        if self.term == 0:
            term = self.i
        if self.term == 1:
            term = self.j
        if self.term == 2:
            term = self.k
        self.term += 1
        return term
        
    def __iter__(self):
        return self

In [33]:
F = Vector(2,3,4)
for comp in F:
    print(comp)


2.0
3.0
4.0

Lastly, we are going to add the ability to compare two vectors and see if they are equivalent. To do this we are going to define the __eq__`` and ``__ne__ methods.


In [34]:
from math import sqrt

class Vector:
    
    def __init__(self, i, j, k=0.0):
        self.i = float(i)
        self.j = float(j)
        self.k = float(k)
        self.mag = sqrt(self.i**2 + self.j**2 + self.k**2)
        self.term = 0
        
    def is_unit_vector(self):
        if self.mag < 1.00000000001 and self.mag > 0.999999999999:
            return True
        else:
            return False
    
    def __add__(self, other): # Vector subtraction: what happens when self + other?
        return Vector(self.i + other.i, self.j + other.j, self.k + other.k)
    
    def __sub__(self, other): # Vector addition: what happens when self - other?
        return Vector(self.i - other.i, self.j - other.j, self.k - other.k)
   
    def __mul__(self, other): # Scalar multiplication: what happens when self * other?
        return Vector(self.i*other, self.j*other, self.k*other)
    
    def __truediv__(self, other): # Scalar division: what happens when self / other?
        return Vector(self.i/other, self.j/other, self.k/other)
    
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

    def __ne__(self, other):
        return not self.__eq__(other)
    
    def __str__(self):
        return('{}i + {}j + {}k'.format(self.i, self.j, self.k))
    
    def __repr__(self):
        return('{}i + {}j + {}k'.format(self.i ,self.j, self.k))
    
    def __next__(self):
        if self.term >= 3:
            raise StopIteration
        if self.term == 0:
            term = self.i
        if self.term == 1:
            term = self.j
        if self.term == 2:
            term = self.k
        self.term += 1
        return term
        
    def __iter__(self):
        return self

In [35]:
K = Vector(1,1,1)
H = Vector(1,1,1)
K == H


Out[35]:
True

In [36]:
K = Vector(1,1,1)
H = Vector(0,1,1)
K == H


Out[36]:
False

In [37]:
K = Vector(1,1,1)
H = Vector(0,1,1)
L = Vector(1,0,0)
R = H + L
R == K


Out[37]:
True

I hope from the two examples above, you have an idea of how to create a new Class in Python. Designing your own classes can be very useful when solving engineering problems with Python.

The chart below details the methods used in the Vector Class

Method Operation Description
__init__ object = Class() create a new object of the Class
__add__ + addition
__sub___ - subtraction
__mul___ * multiplication
__truediv___ / division
__eq___ == equivalent
__ne___ != not equivalent
__str___ print(object) use the print() function
__repr___ object print in the REPL or notebook
__iter___ for a in object iteration
__next___ iteration order