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 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__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]