Python For Quantum Mechanics#

Week 3: Classes#

from IPython.display import YouTubeVideo
YouTubeVideo('Jx7woTztGyA',width=700, height=400)

Defining A Class#

We can think of classes as categories of objects, instances are then particular types of that object. A real life example would be tables. Tables are objects, but we can have many different instances of a table; a white, three-legged table or maybe a brown, four-legged table etc…

Classes contain functions, associated with that class, that we call methods. In the below example hello_method is a method that prints the name associated with a particular instance. The __init__ function is also a method, but it is one of a few so-called magic methods, indicated by the double underscores. It is used to initialise an instance of the class.

Here is how we define a class:

class My_Class:

      statements

For example

class My_Class:
    def __init__(self,name):
        self.name = name
        
    def hello_method(self):
        print('Hello {}!'.format(self.name))
print(type(My_Class))
<class 'type'>
Class_Instance = My_Class("Conor")
print(type(Class_Instance))
<class '__main__.My_Class'>
Class_Instance.hello_method()    
Hello Conor!
Different_Class_Instance = My_Class("Betty")
Different_Class_Instance.hello_method()
Hello Betty!

Notice also the use of self, it simply refers to the instance of the class itself. It can be named arbitrarily, but it is common practice to use self, for example

class My_Class:
    def __init__(fjsdhfg,name):
        fjsdhfg.name = name
        
    def hello_method(fjsdhfg):
        print('Hello {}!'.format(fjsdhfg.name))

        
Class_Instance = My_Class("Conor")
Class_Instance.hello_method()  
Hello Conor!

We can use dir() to obtain a list all the methods in the class instance. Notice all of the magic methods.

print(dir(Class_Instance))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'hello_method', 'name']

Attributes#

There are two types of attributes associated with classes. Namely, class attributes and method attributes.

class My_Class:
    last_name = "Ronan" # This is a class attribute
    def __init__(self,name):
        self.name = name #This is a method attribute
        
    def hello_method(self):
        print('Hello {} {}!'.format(self.name,self.last_name))
Class_Instance = My_Class("Saoirse")
Class_Instance2 = My_Class("Sinead")
Class_Instance.hello_method()
Class_Instance2.hello_method()
Hello Saoirse Ronan!
Hello Sinead Ronan!

Class attributes are accessible by all instances of the class. But method attributes, defined within methods, are only accessible by an instance of the class

#Class attribute
print(Class_Instance.last_name)
print(Class_Instance2.last_name)
Ronan
Ronan
#Method Attribute
print(Class_Instance.name)
print(Class_Instance2.name)
Saoirse
Sinead

Lets define the class in a slighly different way

class My_Class:
    last_name = "Ronan" 
    def __init__(self,name):
        self.n = name #Notice the difference here
        
    def hello_method(self):
        print('Hello {} {}!'.format(self.name,self.last_name))

This begs the questions, how do we call the n or name method attribute. Lets find out.

Class_Instance = My_Class("Saoirse")
print(Class_Instance.n)
Saoirse
print(Class_Instance.name)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[17], line 1
----> 1 print(Class_Instance.name)

AttributeError: 'My_Class' object has no attribute 'name'

As we can see, we must use n, the variable associated with self.

Magic Methods#

Here lets introduce some common magic methods

__init__()#

__init__() automatically initialises the class instance when the instance is generated, while other methods must be called on manually.

class Beep:
    def __init__(self, noise):
        self.noise = noise
        print('Is somebody creating an instance?...I am now initialising...beeepedy beep bop beep {}'.format(noise))
        
    def call_me(self):
        print('Call me Beep {} Bopperson'.format(self.noise))
beeeep = Beep('Bloop')
Is somebody creating an instance?...I am now initialising...beeepedy beep bop beep Bloop
beeeep.call_me()
Call me Beep Bloop Bopperson

In the __init__() method we declare the instance variables bvy assigning them to self. They can then be accessed in other methods using self.

__str__()#

This method is useful for indicating the printable version of an instance of a class.

class Beep:
    def __init__(self, noise):
        self.noise = noise
        print('Is somebody creating an instance?...I am now initialising...beeepedy beep bop beep {}'.format(noise))
        
    def call_me(self):
        print('Call me Beep {} Bopperson'.format(self.noise))
        
    def __str__(self):
        return 'This will print when I print the instance with noise {}'.format(self.noise)
KABLAM = Beep('KABLAM')
Is somebody creating an instance?...I am now initialising...beeepedy beep bop beep KABLAM
print(KABLAM)
This will print when I print the instance with noise KABLAM

Inheritance#

Classes can take on the attributes and methods of other classes by defining them as follows

#class Parent_Class():
    #Satements

#class Child_Class(Parent_Class):
    #Satements

For example

class Being:
    def hello(self):
        print('Hello, I am an intelligent being')

    @property #What is this line? This will be explained...
    def Language(self):
        return 'English'


class Human(Being):
    def hello(self):
        print('Hello, I am an intelligent being...maybe...')


class Alien(Being):
    @property
    def Language(self):
        return 'Glarblackadack'
human = Human()
human.hello()
print("Humans's language is {}".format(human.Language))

alien = Alien()
alien.hello()
print("Alien's language is {}".format(alien.Language))
Hello, I am an intelligent being...maybe...
Humans's language is English
Hello, I am an intelligent being
Alien's language is Glarblackadack

The line @property allows us to override the class method when we inherit that it.

Operator Overloading#

We have use the basic operators +, -, *, / etc, in our calculations and they generally take on their traditional meaning. It turns out that we can reassign them to perform different tasks on objects and their instances. Recall that when we try to add two lists, we get concatenation

L = [1,2,3,4]
M = [5,6,7,8]

S = L + M
print(S)
[1, 2, 3, 4, 5, 6, 7, 8]

Lets attempt to override the + operator so that elements of the list are added not concatenated. To do this, we will create a whole new data type or class called Spooky_List, where adding two Spooky_Lists adds their elements rather than concatenating them.

class Spooky_List():
    def __init__(self,L):
        self.L = L
        self.length = len(L)
        self.temp=L #Creating a temporary list
    
    def __add__(self,other):
        if self.length != other.length:
            print("Lists not the same size")
        else:
            for i in range(self.length):
                self.temp[i] = self.L[i] + other.L[i]
        return self.temp
            
L = [1,2,3,4]
M = [5,6,7,8]

L = Spooky_List(L)
M = Spooky_List(M)

#Note L and M are technically no longer lists but are of type Spooky_List
print(type(L)) 
print(type(M))


S = L+M

print(type(S))
print(S)
<class '__main__.Spooky_List'>
<class '__main__.Spooky_List'>
<class 'list'>
[6, 8, 10, 12]

The __add__ magic method here corresponds to the operator +. Similary we have

Operator

Operator Magic Method

+

__add__(self, other)

-

__sub__(self, other)

*

__mul__(self, other)

/

__truediv__(self, other)

//

__floordiv__(self, other)

%

__mod__(self, other)

**

__pow__(self, other)

>>

__rshift__(self, other)

<<

__lshift__(self, other)

&

__and__(self, other)

|

__or__(self, other)

^

__xor__(self, other)

__getitem__() and __setitem__()#

What happens if we try and find an element of data type Spooky_List

print(L[0])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-30-f05e2cc73309> in <module>
----> 1 print(L[0])

TypeError: 'Spooky_List' object is not subscriptable

We get an error, which makes sense, as there is no reason the methods for lists should work for Spooky_Lists. So lets make our own using __getitem__(). We will also use __setitem__() so that we can assign value to certain indexes in our Spooky_List. Lets also add a __str__() so we can print our Spooky_Lists

class Spooky_List():
    def __init__(self,L):
        self.L = L
        self.length = len(L)
        self.temp=L #Creating a temporary list
    
    def __add__(self,other):
        if self.length != other.length:
            print("Spooky_Lists not the same size")
        else:
            for i in range(self.length):
                self.temp[i] = self.L[i] + other.L[i]
        return self.temp
    
    def __getitem__(self,ind):
        return self.L[ind]
    
    def __setitem__(self,ind, val):
        self.L[ind] = val
        
    def __str__(self):
        return '{}'.format(self.L)
L = [1,2,3,4]
L = Spooky_List(L)

print(type(L))
print(L) #Uses __str__

print(L[0]) #Uses __getitem__

L[0] = 5 #Uses __setitem__
print(L[0]) #Uses __getitem__

print(L) #Uses __str__
<class '__main__.Spooky_List'>
[1, 2, 3, 4]
1
5
[5, 2, 3, 4]