Object oriented programming is a paradigm that makes it easier to manage code, reuse code and allows us to code concepts in terms of objects. Procedural languages allow us to use functions as a way to organize code and reuse code also. Say we need to find the power of a number. We can write few lines of code but what if we need to compute the power in some other section of the code. We do not want to repeat the code ( copy and paste it into the new section ) .
Ex:
....
....
code to computer power of a number
....
some more code
code to computer power of a number
Instead we can do something like:
function power( x , y )
.....
......
Use the function ....
.....
Some more code
......
Use the function
The concept of placing code in different files is also an example of code organization.
It turns out that many of the problems that we write programs for can be structured in terms of classes. We want our implementation to resemble the problem domain as much as possible.
OOP features.
Even though we can say that an Object Oriented language is one that supports the implementation of classes we usually expect the language to support other features such as:
Inheritance
Polymorphism
Operator Overloading
Composition
Constructors
File: Person.py
class Person:
pass
p1 = Person()
p2 = Person()
print( p1 )
print( p2 )
Output:
<__main__.Person object at 0x7ffb6f178f98>
<__main__.Person object at 0x7ffb6f178fd0>
The above is a simple class named "Person" . It has no methods and no properties ( attributes ) . When we print the object out we get the class name and the address in RAM.
Let us add a property and some methods.
File "Person1.py"
class Person:
def setName(self, nameP ) :
self.name = nameP
def getName(self ) :
return ( self.name )
p1 = Person()
p1.setName( "Joe Louis" )
p2 = Person()
p2.setName( "Ezzard Charles" )
print( p1.getName() )
print( p2.getName() )
Output:
[amittal@hills Creation]$ python3 Person1.py
Joe Louis
Ezzard Charles
We add the object properties using the "self" word. When we create a method in a class we need to give a word that denotes the object. Usually the convention is "self" but it could be something else also. In the method "setName" we are stating that assign the "nameP" to my object property "name" . Similarly to get the property out we say "self.name" . Since the property belongs to the object we can access it from outside also.
File "Person2.py"
class Person:
def setName(self, nameP ) :
self.name = nameP
def getName(self ) :
return ( self.name )
p1 = Person()
p1.setName( "Joe Louis" )
p2 = Person()
p2.setName( "Ezzard Charles" )
print( p1.name )
p2.name = "Archie Moore"
print( p2.name )
Output:
[amittal@hills Creation]$ python3 Person2.py
Joe Louis
Archie Moore
Note that we must use the
self.name = nameP
to actually create the variable. We cannot do:
class Person:
def setName(self, nameP ) :
self.name = nameP
def getName(self ) :
return ( self.name )
p1 = Person()
print( p1.name )
p1.setName( "Joe Louis" )
p2 = Person()
p2.setName( "Ezzard Charles" )
print( p1.name )
p2.name = "Archie Moore"
print( p2.name )
Here we are trying to use "p1.name" before "setName" has been called.
We can also create new object variables even after the definition of the class.
File: Person3.py
class Person:
def setName(self, nameP ) :
self.name = nameP
def getName(self ) :
return ( self.name )
p1 = Person()
p1.setName( "Joe Louis" )
p2 = Person()
p2.setName( "Ezzard Charles" )
p1.age = 54
print( p1.name )
print( p1.age )
print( p2.name )
print( p2.age )
Output:
Joe Louis
54
Ezzard Charles
Traceback (most recent call last):
File "Person3.py", line 27, in <module>
print( p2.age )
AttributeError: 'Person' object has no attribute 'age'
We created a new attribute with
p1.age = 54
This only applies to p1. We can see that when we try to use it for p2 we get an error . Not only can our objects have attributes but the class itself
can have attributes . The class attributes are visible to all the objects .
File: Person4.py
class Person:
globalVar = 100
def setName(self, nameP ) :
self.name = nameP
def getName(self ) :
return ( self.name )
p1 = Person()
p1.setName( "Joe Louis" )
p2 = Person()
p2.setName( "Ezzard Charles" )
print( "Person.globalVar: " , Person.globalVar )
print( "p1.name: " , p1.name )
print( "p1.globalVar: ", p1.globalVar )
print("p2.name: " , p2.name )
print( "p2.globalVar: ", p2.globalVar )
Output:
Person.globalVar: 100
p1.name: Joe Louis
p1.globalVar: 100
p2.name: Ezzard Charles
p2.globalVar: 100
We can access the class variable "globalVar" by using the notation
Person.globalVar
or we can access it from the class objects also using the notation :
p1.globalVar
File Person5.py
class Person:
globalVar = 100
def setName(self, nameP ) :
self.name = nameP
def getName(self ) :
return ( self.name )
p1 = Person()
p1.setName( "Joe Louis" )
p2 = Person()
p2.setName( "Ezzard Charles" )
print( "Person.globalVar: " , Person.globalVar )
print( "p1.name: " , p1.name )
p1.globalVar = 200
print( "p1.globalVar: ", p1.globalVar )
print("p2.name: " , p2.name )
print( "p2.globalVar: ", p2.globalVar )
Output:
[amittal@hills Creation]$ python3 Person5.py
Person.globalVar: 100
p1.name: Joe Louis
p1.globalVar: 200
p2.name: Ezzard Charles
p2.globalVar: 100
In the above we changed the value of globalVar by saying that:
p1.globalVar = 200
However we can see that the value of "p2.globalVar" did not get changed. Wait a second. The "globalVar" was a class variable so what happened here. Remember that an object can create it's own instance variables after it is created and that is what happened here.
p1.globalVar = 200
creates a new property ( same name as the class one "globalVar" ) . It does not change the class "globalVar". To change the "globalVar" we need to refer to it using the class name.
File: Person6.py
class Person:
globalVar = 100
def setName(self, nameP ) :
self.name = nameP
def getName(self ) :
return ( self.name )
p1 = Person()
p1.setName( "Joe Louis" )
p2 = Person()
p2.setName( "Ezzard Charles" )
print( "Person.globalVar: " , Person.globalVar )
Person.globalVar = 200
print( "p1.name: " , p1.name )
print( "p1.globalVar: ", p1.globalVar )
print("p2.name: " , p2.name )
print( "p2.globalVar: ", p2.globalVar )
Output:
[amittal@hills Creation]$ python3 Person6.py
Person.globalVar: 100
p1.name: Joe Louis
p1.globalVar: 200
p2.name: Ezzard Charles
p2.globalVar: 200
We access and change the class variable "globalVar" and that change is reflected in the instance objects. The class variable is only created once. When we do something like:
p1 = Person()
p1.globalVar
This starts off a series of actions. . First the "globalVar" is searched for in the instance object. In this case it does not find it there. Then it is looked for in the class .
We have been using the method "setName" in our Person class . Let's say that we require each Person object to have a name. In our implementation we can forget to call the "setName" method and this is where the concept of a "constructor" comes in. The constructor is a special method that will be called when we are creating an object. The method name must be "__init__" . The method names with "__" at the beginning and the end are special names . Let us change the "Person" class to work with a constructor.
Another example.
File tut1.py
class MyClass:
x1 = 5
var1 = MyClass()
var2 = MyClass()
print( var1.x1 )
var1.x1 = 10
print( var2.x1 )
#Class attribute
#print( MyClass.x1 )
MyClass.x1 = 20
print( var2.x1 )
print( var1.x1 )
#var1.x1 = 10
#print( var1.x1 )
#print( MyClass.x1 )
File: Person7.py
class Person:
globalVar = 100
def __init__( self , nameP ) :
self.name = nameP
def setName(self, nameP ) :
self.name = nameP
def getName(self ) :
return ( self.name )
p1 = Person( "Jake LaMotta" )
p2 = Person("Marcel Cerdan")
print( "p1.name: " , p1.getName() )
print("p2.name: " , p2.getName() )
Output:
p1.name: Jake LaMotta
p2.name: Marcel Cerdan
We define the constructor using the special "__init__" method call. Now if we forget to initialize the object while constructing it we will see an error:
File Person8.py
class Person:
globalVar = 100
def __init__( self , nameP ) :
self.name = nameP
def setName(self, nameP ) :
self.name = nameP
def getName(self ) :
return ( self.name )
p1 = Person( )
p2 = Person("Marcel Cerdan")
print( "p1.name: " , p1.getName() )
print("p2.name: " , p2.getName() )
Output:
[amittal@hills Creation]$ python3 Person8.py
Traceback (most recent call last):
File "Person8.py", line 16, in <module>
p1 = Person( )
TypeError: __init__() missing 1 required positional argument: 'nameP'
Notice how we are using "getName" function to print the name. If we try to print the "Person" object we will get something like.
File: Person9.py
class Person:
globalVar = 100
def __init__( self , nameP ) :
self.name = nameP
def setName(self, nameP ) :
self.name = nameP
def getName(self ) :
return ( self.name )
p1 = Person("Gene Fullmer" )
print( p1 )
Output:
[amittal@hills Creation]$ python3 Person9.py
<__main__.Person object at 0x7f91135467b8>
[amittal@hills Creation]$
Wouldn't it be nice if we could print the "Person" object and have it print something more meaningful than the just the class name and the address. This is where we can make use of operator overloading. Operator overloading means that we can write a function and when we use the object then that function will get called. The usage is more natural and what we are used to when dealing with build in types. Our intent is that the custom classes can be used in a similar fashion to built in types.
The overloaded operator we are going to use is "__str__" and it's called when we expect a string to be returned from our using the object.
File: Person10.py
class Person:
globalVar = 100
def __init__( self , nameP ) :
self.name = nameP
def __str__(self) :
return( "Name is:" + self.name )
p1 = Person("Samuel Peters" )
print( p1 )
Output:
[amittal@hills Creation]$ python3 Person10.py
Name is:Samuel Peters
Exercises
Ex1 :
Write the code for the constructor( "__init__" ) and the "__str__" methods.
class Vechicle:
vehicles_created = 0
def __init__(self, weight, top_speed, description ) :
def __str__(self) :
v1 = Vechicle( 200, 100, "car" )
v2 = Vechicle( 100, 80, "moped" )
print( v1 )
print( v2 )
print( Vechicle.vehicles_created )
Your program should print:
Weight:200 Top Speed:100 Description:car
Weight:100 Top Speed:80 Description:moped
2
Soln1 :
class Vechicle:
vehicles_created = 0
def __init__(self, weight, top_speed, description ) :
self.weight = weight
self.top_speed = top_speed
self.description = description
Vechicle.vehicles_created = Vechicle.vehicles_created + 1
def __str__(self) :
return ( "Weight:" + str(self.weight) + " Top Speed:" + str(self.top_speed) +
" Description:" + self.description )
v1 = Vechicle( 200, 100, "car" )
v2 = Vechicle( 100, 80, "moped" )
print( v1 )
print( v2 )
print( Vechicle.vehicles_created )
We can organize our code using modules.
Ex
File: module1.py
var1 = 400
def makeUpper( str1 ) :
return str1.upper()
We have a variable and a function "makeUpper" defined in a file called "module1.py" . We can access these from another file .
File: user.py
from module1 import makeUpper
from module1 import var1
print( makeUpper( "test" ) )
print( var1 )
Using the "from" statement I specify the file name "module1" and then import the specific items from that module. I can then use and access those variables and functions. There is a way to import everything using the "*" statement.
File: user_a.py
from module1 import *
print( makeUpper( "test" ) )
print( var1 )
Now this brings in everything. However remember the things are getting added to the global namespace in the file "user_a.py" also.
Let us look at another way to import stuff from the file "module1"
File: user1.py
import module1
print( module1.makeUpper( "test" ) )
print( module1.var1 )
We used the statement import "module1" . Now we access the items by placing the "module1" word in front of them . This has the advantage that we are not polluting the global namespace.
Exercises
1.
Create a file called "graph.py" that has a class called "Point" . This represents a point in the 2 dimensional graph. Save the x1 and y1 in the class constructor.
There is also an overloaded "__add__" method that takes in another point object. This method should return a new Point object that has x1 and y1 are the sums of the
x1, y1 in the object itself and the point1 object.
Write the code for the following methods
File: graph.py
class Point:
def __init__(self, x1, y1) :
def __str__(self) :
def __add__(self, point1) :
Use the below file to test out your class
File: use_point.py
import graph
p1 = graph.Point( 1,1 )
p2 = graph.Point( 2,2 )
p3 = p1 + p2
print( p3 )
Output:
[amittal@hills class]$ python3 use_point.py
x cordinate is:3 y cordinate is:3
Solutions
1
class Point:
def __init__(self, x1, y1) :
self.x1 = x1
self.y1 = y1
def __str__(self) :
return( "x cordinate is:" + str(self.x1) +
" y cordinate is:" + str(self.y1) )
def __add__(self, point1) :
return( Point( self.x1 + point1.x1, self.y1 + point1.y1 ) )
:
We can place classes in separate modules.
Modules are a good way to organize your code using files. If we have any code in "graph.py" then that code will be executed once we import the file to be used from another program such as "use_point.py" . Sometimes we don't want that. We want the code to be inside "graph.py" but do not want that code to be run during an import. We can accomplish that using the
"__main__" value assigned to the property "__name__" . .
This is set to "__main__" if we run the file itself and set to the file name if we import the file.
Ex:
class Point:
def __init__(self, x1, y1) :
self.x1 = x1
self.y1 = y1
def __str__(self) :
return( "x cordinate is:" + str(self.x1) +
" y cordinate is:" + str(self.y1) )
def __add__(self, point1) :
return( Point( self.x1 + point1.x1, self.y1 + point1.y1 ) )
print( "Outside Main: Name : " , __name__ )
if __name__ == '__main__':
print( "Inside Main" )
When we run the above code then we get:
Outside Main: Name : __main__
Inside Main
We can see that the "name" acquired the value "__main__" and both the print outside the "if" condition and inside the "if" condition were printed. Now we are going to call the same file from "use_point.py" .
[amittal@hills main]$ python3 use_point.py
Outside Main: Name : graph
x cordinate is:3 y cordinate is:3
Now we can see only the "print" statements outside the "if" condition were printed and the "__name__" actually acquired another value; "graph" . This way we can place our testing code and other code that we do not want run when we import the module.
We have seen how at runtime we can add properties to both the class and an object.
File: b1.py
class FirstClass : # Define a class object
def setdata(self, value): # Define class's methods
self.data = value # self is the instance
def display(self):
print(self.data)
FirstClass.global1 = 200
x1 = FirstClass()
x1.global2 = 300
print( x1.global1 )
print( x1.global2 )
Output:
[amittal@hills Dynamic]$ python3 b1.py
200
300
In addition to adding properties at runtime we can also add methods at run time.
import types
class FirstClass : # Define a class object
def setdata(self, value): # Define class's methods
self.data = value # self is the instance
def display(self):
print(self.data)
def Function1( self ) :
print( "Inside Function1: " + str(self.data) )
def Function2( self ) :
print( "Inside Function2: " + str(self.data) )
x1 = FirstClass()
x1.setdata( 100 )
FirstClass.f1 = Function1
#x1.f2 = Function2
x1.f1()
#x1.f2()
#Gives us an error of the form:
#
#TypeError: Function2() missing 1 required positional argument: 'self'
x1.f2 = types.MethodType( Function2, x1 )
x1.f2()
The above program shows how we added a function "Function1" to the class. Adding a function dynamically is slightly more work for x1. We have to import a system module called "types" and then use the statement :
x1.f2 = types.MethodType( Function2, x1 )
to assign the function "Function2" to the "x1" property/function "f2" .
File: p1.py
class Person:
g1 = 100
def __init__(self, id1) :
self.id1 = id1
p1 = Person(5)
print( p1.__dict__ )
print( "-------------" )
print ( Person.__dict__ )
print( "-------------" )
print( p1.__class__ )
print( "-------------" )
print( Person.__bases__ )
Output:
{'id1': 5}
-------------
{'__module__': '__main__', 'g1': 100, '__init__': <function Person.__init__ at 0x7f0f2bc4cea0>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
-------------
[amittal@hills Architect]$ python3 p1.py
{'id1': 5}
-------------
{'__module__': '__main__', 'g1': 100, '__init__': <function Person.__init__ at 0x7fe63f971ea0>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
-------------
<class '__main__.Person'>
-------------
(<class 'object'>,)
The above program provides some insights into how the Python implements the classes and objects concept. A class and an instance are both objects . The instance object uses a dictionary to store the parameters and their values. We know that if it cannot find the property then it looks in the class. The class also has a dictionary object to store the properties and the values. We cab see that it stores "g1" as the key and the 100 as the value.
Continue with vehicle
File "1.py"
class Vechicle:
vehicles_created = 0
def __init__(self, weight, top_speed, description ) :
self.weight = weight
self.top_speed = top_speed
self.description = description
Vechicle.vehicles_created = Vechicle.vehicles_created + 1
def __str__(self) :
return ( "Weight:" + str(self.weight) + " Top Speed:" + str(self.top_speed) +
" Description:" + self.description )
class Train( Vechicle ) :
def __init__(self, weight, top_speed, description, noCarriages ) :
super().__init__( weight, top_speed, description )
self.noCarriages = noCarriages
# def __str__(self) :
# return ( "Weight:" + str(self.weight) + " Top Speed:" + str(self.top_speed) +
# " Description:" + self.description + " No Carriages:" + str(self.noCarriages) )
def __str__(self) :
return( super().__str__() + " No Carriages:" + str(self.noCarriages) )
t1 = Train( 100, 200,"Train" , 10 )
print( t1 )
The above program shows a derived class "Train" that inherits from the Vehicle class .
File "attr1.py"
class Base:
x1 = 200
def __init__( self, param1 ) :
self.x1 = 200
def getBaseVariable(self) :
return self.x1
class Derived( Base ) :
x1 = 100
def __init__( self, param1 ) :
super().__init__( param1 )
self.x1 = param1
d1 = Derived(10)
#Print derived class d1's x1 should print 100
#Print d1's x1 should print 10
print( d1.x1 )
print( Derived.x1 )
#print Base's x1
print( Base.x1 )
#print Base object's x1
print( d1.getBaseVariable() )
Output:
[amittal@hills Inheritance]$ python3 att1.py
10
100
200
10
We cannot have multiple "__init__" constructors in a class. However we can assign default values to certain parameters.
File: " Person.py"
class Person:
def __init__( self, name, job=None, pay=0 ): # Normal function args
self.name = name
self.job = job
self.pay = pay
def __str__(self) :
return( "Name:" + self.name + " Job:" + str( self.job) + " Pay:" + str(self.pay) )
#The pay defaults to 0
p1 = Person( "John" , ["Engr"] )
print( p1 )
#We want 100 to refer to pay ... does not work that way.
p2 = Person( "John" , 100)
print( p2 )
In the above we set default values for
def __init__( self, name, job=None, pay=0 ):
for job and pay . That means if we only specify the value for name then job and pay will assume the values of "None" and 0 . Once we set a default value then the rest of the variables on the right hand side need the default values also. We cannot do:
def __init__( self, name="Ajay", job, pay=0 ): # Normal function args
Otherwise we will see an error:
[amittal@hills chapter28]$ python3 Person1.py
File "Person1.py", line 2
def __init__( self, name="Ajay", job, pay=0 ): # Normal function args
^
SyntaxError: non-default argument follows default argument
If we were allowed to do that then how about the case :
name="John" , job, pay=0
We called the function with a single argument of "Engr"
The compiler can assign that to "job" and the name and pay take on default values. But what if we wanted to assign the name "Steve" to "name" and "Engr" to the "job" . So we specify 2 arguments but does Python assign them to "name", "job" or "job" , "pay" . To avoid all these possibilities the rule is that whatever arguments we specify are assigned left to right and if you assign a default value then rest of the arguments on the right hand side must take default values also.
We can skip an argument in the middle by explicitly specifying the parameter name.
Ex:
class Person:
def __init__( self, name, job=None, pay=0 ): # Normal function args
self.name = name
self.job = job
self.pay = pay
def __str__(self) :
return( "Name:" + self.name + " Job:" + str( self.job) + " Pay:" + str(self.pay) )
#The pay defaults to 0
p1 = Person( name="John" , pay=100 )
print( p1 )
The word "self" always refers to the original object that was constructed.
Ex:
File: "i2.py"
class Person:
def display(self) :
print( "Manager:" , self.idOfManager )
class Manager( Person ) :
def __init__( self, idOfManager ):
self.idOfManager = idOfManager
m1 = Manager(55)
m1.display()
p1 = Person()
p1.display()
Output:
[amittal@hills Inspect]$ python3 i2.py
Manager: 55
Traceback (most recent call last):
File "i2.py", line 20, in <module>
p1.display()
File "i2.py", line 4, in display
print( "Manager:" , self.idOfManager )
AttributeError: 'Person' object has no attribute 'idOfManager'
[amittal@hills Inspect]$
We create a manager object "m1" in
m1 = Manager( 55 )
The Manager class has an id :
idOfManager
and that gets set in the Manger class. We then call "display" which is not a method of the Manager class but the Person class. However we can see that the base class is able to access the derived class variable because we used the "self" word. : However the "Person" object does not have an id "idOfMAnager" and we get an error out.
File: i4.py
class Manager:
def display(self) :
print( "Manager:" , self.__class__ )
print( "----" )
class Person( Manager ) :
counter = 5
def function1( self ):
print( "In Person class:" , self.__class__ )
p1 = Person()
#The __class_ is not a class attribute !
print( Person.__class__ )
print( p1.function1() )
#print( "++++++++++++" )
print( p1.__class__ )
#From a class we can get the base of a class
print( p1.__class__.__bases__ )
print( Person.__bases__ )
print( "-------------" )
print( p1.__dict__ )
print( Person.__dict__ )
#What can we conclude from the above
# __class__ is a property of the object
# __bases__ is a property of the class
In the code above we show how the "class" attribute and the "bases", "dict" attributes can be used.
File: "i3.py"
class AttrDisplay:
"""
Provides an inheritable display overload method that shows
instances with their class names and a name=value pair for
each attribute stored on the instance itself (but not attrs
inherited from its classes). Can be mixed into any class,
and will work on any instance.
"""
def gatherAttrs(self):
attrs = []
for key in sorted(self.__dict__):
attrs.append('%s=%s' % (key, getattr(self, key)))
return ', '.join(attrs)
def __repr__(self):
return '[%s: %s]' % (self.__class__.__name__, self.gatherAttrs())
class Person( AttrDisplay ) :
def display(self) :
print( "Manager:" , self.idOfManager )
class Manager( Person ) :
globalVar1 = 10
def __init__( self, idOfManager ):
self.idOfManager = idOfManager
m1 = Manager(55)
print( m1 )
#m1.display()
#p1 = Person()
#p1.display()
The above uses the class "AttrDisplay" from the book to print out our "Manager" class.
print( p1.__class__.__bases__[0].__dict__ )
We can access the :base" class using the above expression and then use it's "dict" property to print out the attributes.
Composition is the process whereby a class can contain objects of other class .
The below code has a logger class that writes a message to a file.
Ex:
1)
import datetime
class myDateTime:
#0 default one with 24 hours
#1 with 12 hour and ampm
formatType = 0
def getFormattedStringDefault(self) :
self.d1 = datetime.date.today()
self.hour = datetime.datetime.now().hour
self.minutes = datetime.datetime.now().minute
self.seconds = datetime.datetime.now().second
str1 = ( str(self.d1.day) + "_" + str(self.d1.month) + "_" +
str(self.d1.year) + ":" + str(self.hour) + "_" + str(self.minutes ) +
"_" + str(self.seconds) )
#print( "str1:" , str1 )
#print ( "Inside func: 1" , type( str1 ) )
return( str(str1) )
def getFormattedStringAmpm(self) :
#To do
def getFormattedString(self) :
if ( myDateTime.formatType == 0 ) :
#print( "Step1" )
return( self.getFormattedStringDefault() )
else:
return( self.getFormattedStringAmpm() )
#date1 = myDate()
class logger:
def __init__( self, fileName) :
self.fileNameP = fileName
self.d1 = myDateTime()
def formatL( self , message ) :
self.str1a = self.d1.getFormattedString()
#print( "self.str1a:" , self.str1a )
self.str1 = self.d1.getFormattedString() + " :" + message + "\r\n"
#print( "Logger str1:" + self.str1 )
return( self.str1 )
def logMessage( self, message ) :
f1 = open ( self.fileNameP, "a+" )
f1.write( self.formatL( message ) )
f1.close()
l1 = logger( "log.txt" )
#print( l1.formatL( "Testing" ) )
#print ( l1.format( "Line1" ) )
l1.logMessage( "Line1" )
l1.logMessage( "Line2" )
#l1.logMessage( "Line2" )
2)
Fill in the class methods for Tires and Engines and Vehicle. Create 2 objects and print them . Store the objects in a database file called "db.txt" using the "shelve" package.
Create another file called use_vehicle.py to read the objects that were written to the file.
#Constructor takes noOfTires brandName
class tire :
def __init__
def __str__
#Constructor takes noOfCC gasOrDiesel, manFacturerName
class engine :
def __init__
def __str__
#Constructor takes tireObj, engineObj
class vehicle :
def __init__
def __str__
tireObj1 = tire( 4 , "FireBird" )
engineObj1 = engine( 1600 , "gas" , "Dodge" )
vehicleObj1 = vehicle( tireObj1, engineObj1 )
tireObj2 = tire( 4 , "Michelin" )
engineObj2 = engine( 1400 , "gas" , "Mercedez" )
vehicleObj2 = vehicle( tireObj2, engineObj2 )
print( vehicleObj1 )
print( vehicleObj2 )
1)
import datetime
class myDateTime:
#0 default one with 24 hours
#1 with 12 hour and ampm
formatType = 1
def getFormattedStringDefault(self) :
self.d1 = datetime.date.today()
self.hour = datetime.datetime.now().hour
self.minutes = datetime.datetime.now().minute
self.seconds = datetime.datetime.now().second
str1 = ( str(self.d1.day) + "_" + str(self.d1.month) + "_" +
str(self.d1.year) + ":" + str(self.hour) + "_" + str(self.minutes ) +
"_" + str(self.seconds) )
#print( "str1:" , str1 )
#print ( "Inside func: 1" , type( str1 ) )
return( str(str1) )
def getFormattedStringAmpm(self) :
self.d1 = datetime.date.today()
self.hour = datetime.datetime.now().hour
self.minutes = datetime.datetime.now().minute
self.seconds = datetime.datetime.now().second
self.ampm = "AM"
if ( self.hour >= 12 ) :
self.ampm = "PM"
if ( self.hour > 12 ) :
self.hour = self.hour - 12
str1 = ( str(self.d1.day) + "_" + str(self.d1.month) + "_" +
str(self.d1.year) + ":" + str(self.hour) + "_" + str(self.minutes ) +
"_" + str(self.seconds) + ":" + self.ampm )
return( str(str1) )
def getFormattedString(self) :
if ( myDateTime.formatType == 0 ) :
#print( "Step1" )
return( self.getFormattedStringDefault() )
else:
return( self.getFormattedStringAmpm() )
#date1 = myDate()
class logger:
def __init__( self, fileName) :
self.fileNameP = fileName
self.d1 = myDateTime()
def formatL( self , message ) :
self.str1a = self.d1.getFormattedString()
#print( "self.str1a:" , self.str1a )
self.str1 = self.d1.getFormattedString() + " :" + message + "\r\n"
#print( "Logger str1:" + self.str1 )
return( self.str1 )
def logMessage( self, message ) :
f1 = open ( self.fileNameP, "a+" )
f1.write( self.formatL( message ) )
f1.close()
l1 = logger( "log.txt" )
#print( l1.formatL( "Testing" ) )
#print ( l1.format( "Line1" ) )
l1.logMessage( "Line1" )
l1.logMessage( "Line2" )
#l1.logMessage( "Line2" )
2)
import shelve
class tire :
def __init__( self, noOfTires, brandName ) :
self.noOfTires = noOfTires
self.brandName = brandName
def __str__( self ) :
return( "Tire info: noOfTires: " + str(self.noOfTires) + " brandName:" +
str(self.brandName) )
class engine :
def __init__( self, noOfCC, gasOrDiesel, manFacturerName ) :
self.noOfCC = noOfCC
self.gasOrDiesel = gasOrDiesel
self.manFacturerName = manFacturerName
def __str__( self ) :
return( "Engine info: noOfCC: " + str(self.noOfCC) + " gasOrDiesel:" +
str(self.gasOrDiesel) + " manFacturerName:" +
str(self.manFacturerName) )
class vehicle :
def __init__( self, tireObj, engineObj ) :
self.tireObj = tireObj ;
self.engineObj = engineObj ;
def __str__( self ) :
return( "Vechicle info: " + str(self.tireObj) +
" " + str(self.engineObj) )
tireObj1 = tire( 4 , "FireBird" )
engineObj1 = engine( 1600 , "gas" , "Dodge" )
vehicleObj1 = vehicle( tireObj1, engineObj1 )
tireObj2 = tire( 4 , "Michelin" )
engineObj2 = engine( 1400 , "gas" , "Mercedez" )
vehicleObj2 = vehicle( tireObj2, engineObj2 )
db = shelve.open( "db.txt" )
db["1"] = vehicleObj1
db["2"] = vehicleObj2
db.close()
print( vehicleObj1 )
print( vehicleObj2 )
File: use_vehicle1.py
import shelve
import vehicle1
db = shelve.open( "db.txt" )
vehicle1.vehicleObj1 = db["1"]
vehicle1.vehicleObj2 = db["2"]
db.close()
print( vehicleObj1 )
print( vehicleObj2 )
Python has a built in module called "shelve" that gives us the ability to serialize class objects and store them to a file by a key. The key can be any string.
Ex: Person.py
import shelve
class Person:
def __init__( self, name, job=None, pay=100 ): # Normal function args
self.name = name
self.job = job
self.pay = pay
def __str__(self) :
return( "Name:" + self.name + " Job:" + str( self.job) + " Pay:" + str(self.pay) )
p1 = Person( "John" , "Engr" )
p2 = Person( "Anthony" , "Teacher", 300)
p3 = Person( "Joshua" , "Electrician" )
db = shelve.open( "db.txt" )
for obj in ( p1, p2, p3 ) :
db[ obj.name ] = obj
db.close()
Reading the stored records back from the file.
File: Client1.py
import Person
import shelve
db = shelve.open( 'db.txt' ) # Reopen the shelve
print( len(db) ) # Three 'records' stored
print( list(db.keys()) ) # keys is the index
print( db["John"] )
db.close()
Ex: Person1.py
Storing and loading the objects in the same file
import shelve
class Person:
def __init__( self, id, name ): # Normal function args
self.id = id
self.name = name
def __str__(self) :
return( "Name:" + self.name + " Id:" + str( self.id) )
p1 = Person( 1 , "John" )
p2 = Person( 2 , "Anthony" )
if __name__ == "__main__" :
db = shelve.open( "db.txt" )
db[ "1" ] = p1
db[ "2" ] = p2
db.close()
db = shelve.open( 'db.txt' ) # Reopen the shelve
print( len(db) ) # Three 'records' stored
print( list(db.keys()) ) # keys is the index
p1 = db["1"]
print( p1 )
p2 = db["2"]
print( p2 )
db.close()
Operator Overloading
Point Example
File: Person.py
class Person:
def __str__( self ) :
return ( str(self.id1) )
def __init__( self, id1 ) :
self.id1 = id1
def __eq__( self, person1 ) :
return ( self.id1 == person1.id1 )
def __ge__( self, person1 ) :
return ( self.id1 >= person1.id1 )
def __gt__( self, person1 ) :
return ( self.id1 > person1.id1 )
def __le__( self, person1 ) :
return ( self.id1 <= person1.id1 )
def __lt__( self, person1 ) :
return ( self.id1 < person1.id1 )
def __ne__( self, person1 ) :
return ( self.id1 != person1.id1 )
p1 = Person( 10 )
p2 = Person( 2 )
p3 = Person( 6 )
p4 = Person( 5 )
list1 = [ p1 , p2, p3 ,p4 ]
list1.sort()
for x1 in list1 :
print( x1 )
File: Point.py
class Point:
def __init__(self, x1, y1) :
self.x1 = x1
self.y1 = y1
def __str__(self) :
return( "x cordinate is:" + str(self.x1) +
" y cordinate is:" + str(self.y1) )
def __add__(self, point1) :
return( Point( self.x1 + point1.x1, self.y1 + point1.y1 ) )
p1 = Point( 2, 2 )
p2 = Point( 1, 1 )
p3 = p1 + p2
print( p3 )
File: oper1.py
class MyVector:
def __init__(self, listObj):
print( "Inside Init" )
self.list1 = listObj
def __getitem__(self, key):
print( "Inside getItem" )
return 5
#return self.list1[key]
def __add__(self, item1):
print( "Inside add" )
self.list1.append( item1 )
return self.list1
def display(self):
print( "MyVector" )
def displayOutsideClass():
print( "MyVector" )
list2 = [ 10, 20 ]
v1 = MyVector( list2 )
#print( v1.list1 )
#v1.display()
#print( v1.list1 )
print( v1[0] )
list2 = v1 + 30
print( v1.list1 )
#print( list2 )
Abstract Classes
An abstract class in terms of oop is defined as a class that we cannot make an instance of. Let's look at the following program.
File "technique1.py"
class Super:
def method(self):
print('in Super.method') # Default behavior
def delegate(self):
self.action() # Expected to be defined
class Inheritor( Super ): # Inherit method verbatim
def __init__( self ) :
print( "Inheritor constructor" )
# pass
class Replacer(Super): # Replace method completely
def method(self):
print('in Replacer.method')
class Extender(Super): # Extend method behavior
def method(self):
print('starting Extender.method')
Super.method(self)
print('ending Extender.method')
class Provider(Super): # Fill in a required method
def action(self):
print('in Provider.action')
if __name__ == '__main__':
for klass in (Inheritor, Replacer, Extender):
print('\n' + klass.__name__ + '...')
klass().method()
print('\nProvider...')
x = Provider()
x.delegate()
Output:
[amittal@hills chapter29]$ python3 technique1.py
Inheritor...
Inheritor constructor
in Super.method
Replacer...
in Replacer.method
Extender...
starting Extender.method
in Super.method
ending Extender.method
Provider...
in Provider.action
The above shows how derived classes are getting created. The provider class calls the method "delegate"
x = Provider()
x.delegate()
This in turn calls the base class method :
def delegate(self):
self.action()
The "self.action()" ends up calling the "Provider" action method.
File: "technique2.py"
class Super:
def method(self):
print('in Super.method') # Default behavior
def delegate(self):
self.action() # Expected to be defined
class Inheritor( Super ): # Inherit method verbatim
def __init__( self ) :
print( "Inheritor constructor" )
# pass
class Replacer(Super): # Replace method completely
def method(self):
print('in Replacer.method')
class Extender(Super): # Extend method behavior
def method(self):
print('starting Extender.method')
super().method() #We can call the super method this way also
print('ending Extender.method')
class Provider(Super): # Fill in a required method
def action(self):
print('in Provider.action')
if __name__ == '__main__1':
for klass in (Inheritor, Replacer, Extender):
print('\n' + klass.__name__ + '...')
klass().method()
print('\nProvider...')
x = Provider()
x.delegate()
#Even though Super has a method that has not been
#defined we can still create an instance of Super
s1_obj = Super()
#s1_obj.delegate()
#We can also create an object of a derived class
#that does not implement the action method
e1Obj = Extender()
#e1Obj.delegate()
The abstract class could do:
class Super:
def delegate(self):
self.action()
def action(self):
assert False, 'action must be defined!' # If this version is called
Let's see what happens when we create an object of the derived class and call the delgate method on it.
File: "technique4.py"
class Super:
def method(self):
print('in Super.method') # Default behavior
def delegate(self):
self.action() # Expected to be defined
def action( self ) :
assert False, 'action must be defined!'
class Inheritor( Super ): # Inherit method verbatim
def __init__( self ) :
print( "Inheritor constructor" )
# pass
class Replacer(Super): # Replace method completely
def method(self):
print('in Replacer.method')
class Extender(Super): # Extend method behavior
def method(self):
print('starting Extender.method')
super().method() #We can call the super method this way also
print('ending Extender.method')
class ProviderBase( Super ): # Fill in a required method
def action(self):
print('in Provider Base.action')
class Provider( ProviderBase ): # Fill in a required method
pass
i1Obj = Inheritor()
i1Obj.action()
Assert
The assert can take 2 arguments and the 2nd argument can be optional.
File: "assert2.py"
def avg(marks):
assert len(marks) != 0
return sum(marks)/len(marks)
mark1 = []
print("Average of mark1:",avg(mark1))
Output:
Traceback (most recent call last):
File "assert2.py", line 6, in <module>
print( "Average of mark1:", avg(mark1) )
File "assert2.py", line 2, in avg
assert len(marks) != 0
AssertionError
We use "assert" to make sure that if a certain condition is met then our program should stop . In the above code we want to make sure that the marks list is empty and if it is then an "Assertion Error" is thrown. What is the difference between assertions and exception handling ? Assertions are used to check conditions that should never occur where as with exception handling we have a chance to recover. We can use assertion in test code also. If some test fails then the program stops and the assertion error is printed out.
File: "assert1.py"
#False condition
assert False , "Some error"
x1 = 4
x2 = 3
assert x1 == x2 , "Values are not equal"
x2 = 4
assert x1 == x2 , "Values are equal"
Output:
Traceback (most recent call last):
File "assert1.py", line 4, in <module>
assert False , "Some error"
AssertionError: Some error
File: "assert1.py"
#False condition
# assert False , "Some error"
x1 = 4
x2 = 3
assert x1 == x2 , "Values are not equal"
x2 = 4
assert x1 == x2 , "Values are equal"
Output:
Traceback (most recent call last):
File "assert1.py", line 9, in <module>
assert x1 == x2 , "Values are not equal"
AssertionError: Values are not equal
File: "assert1.py"
#False condition
# assert False , "Some error"
x1 = 4
x2 = 3
#assert x1 == x2 , "Values are not equal"
x2 = 4
assert x1 == x2 , "Values are equal"
Output:
In the above "x1" is equal to "x2" so nothing gets printed. The second part is only executed if the first condition is false. Since we have the statement:
x1 = 4
x2 = 4
the last part
assert x1 == x2 , "Values are equal"
The "x1 == x2" is true so the second part is not executed.
1)
File: "ex1.py"
from abc import ABCMeta, abstractmethod
class Shape(metaclass=ABCMeta):
@abstractmethod
def draw(self):
pass
class Circle( Shape ) :
#Set the radius to the circle object
def __init__( self , radius ) :
#print "Circle: Draw"
def draw(self):
class Square ( Shape ) :
#Set the width to the square object
def __init__( self , width ):
#print "Square: Draw"
def draw(self):
c1 = Circle( 10 )
sq1 = Square( 20 )
c1.draw()
sq1.draw()
1)
from abc import ABCMeta, abstractmethod
class Shape(metaclass=ABCMeta):
@abstractmethod
def draw(self):
pass
class Circle( Shape ) :
def __init__( self , radius ) :
self.radius = radius
def draw(self):
print( "Circle: Draw" )
class Square ( Shape ) :
def __init__( self , width ):
self.width = width
def draw(self):
print( "Square: Draw" )
c1 = Circle( 10 )
sq1 = Square( 20 )
c1.draw()
sq1.draw()