Python Programming Fundamentals for Class 11 and 12 – Object Oriented Programming

Object-oriented programming (OOP) is a programming paradigm that represents concepts as “objects”, that have attributes which describe the object in the form of data attributes and associated procedures known as methods. As mentioned in chapter 1, Python is an OOP language. In Python, class form the basis of OOP. Some of the features of OOP language are:

  • Inheritence
  • Polymorphism
  • Encapsulation

Some of the advantages of OOP approach are:

  • Reusability: A part of a code can be reused for accommodating new functionalities with little or no changes.
  • Maintenance: If some modification is made in base class, the effect gets reflected automatically into the derived class, thus, the code maintenance is significantly less hectic.
  • Faster output: With organized and methodical coding, there is little room for error, and as a result programmer can work comfortably, resulting in fast and efficient output.

Class
A class is the particular object type created by executing a class statement. Class objects are used as templates to create instance objects, which embodies the attributes: the “data attributes” and “methods”, specific to a data type. A class definition is given below:

classdef ::= "class" classname [inheritance]":"suite
 inheritance ::= "(" [expression_list] ")"
 classname ::= identifier

The above class definition might seem alien, it will become more clear with the progress of this chapter. The simplest form of class definition looks like:

class ClassName:
 <statement-1>
 .
 .
 <statement-N>

The following example gives a glimpse of how a class is defined.

>>> class Output:
 ... def Display(self):
 . . . print 'This is a class example.'
 ...
 >>> x=Output()
 >>> x . Display ()
 This is a class example.

Like function definition (def statements), the class definition (Output in the above example) must be executed before they have any effect. In practice, the statements inside a class definition will usually be function (or more specifically “method”) definitions (Display () in the above example), but other statements are allowed. The function definitions inside a class normally have a peculiar form of argument list, dictated by the calling conventions for methods (discussed later).
Creation of a class definition also creates a new namespace, and used as the local scope, thus all assignments to local variables go into this new namespace.

Method
A method is a function that belongs to an object. In Python, the term “method” is not unique to class instance, other object types can have methods as well. For example, list objects have methods, namely, append, insert, remove, sort, and so on.
Usually in a class, the method is defined inside its body. If called as an attribute of an instance of that class, the method will get the instance object as its first argument (which is usually called self). Self is merely a conventional name for the first argument of a method. For example, a method defined as meth (self,a,b,c) should be called as x .meth (a, b, c) for an instance x of the class in which the definition occurs; the called method will think it is called as meth (x, a, b, c). The idea of self was borrowed from “Modula-3” programming language.
It is not necessary that the function definition is textually enclosed in the class definition; assigning a function object to a local variable in the class is also fine. For example:

>>> def f1(self, x, y) :
 ... return min(x, x+y)
 ...
 >>> class test_class:
 . . . aa=f1
 ... def bb(self) :
 ... return 'hello world'
 ... cc=bb
 >>>

Here aa, bb and cc are all attributes of class test_class that refer to function objects, and consequently, they are all methods of instances of class test_class; cc being exactly equivalent to bb. Note that this practice usually confuses the reader of the program.

Usually, Python use methods for some functionality (e.g. list. index ()), but functions for other (e.g. len (list)). The major reason is history; functions were used for those operations that were generic for a group of types and which were intended to work even for objects that did not have methods at all (e.g. tuples). In fact, implementing len (), max (), min () etc. as built-in functions has actually less code than implementing them as methods for each type. One can quibble about individual cases, but it is part of Python, and it is too late to make such fundamental changes now. The functions have to remain to avoid massive code breakage.

Class object
When a class definition is created, a “class object” is also created. The class object is basically a wrapper around the contents of the namespace created by the class definition. Class object support two kinds of operations: “attribute reference” and “instantiation”.

Attribute reference
Attribute references use the standard syntax used for all attribute references in Python: obj .name. Valid attribute names are all the names that were in the class’s namespace, when the class object was created. So, if the class definition looked like this:

>>> class MyClass:
 ... """A simple example class"""
 ... i=12345
 ... def f(self):
 ... return 'hello world'
 >>> MyClass.i
 12345 '
 >>> MyClass. _doc_
 'A simple example class'

then MyClass . i and MyClass . f are valid attribute references, returning an integer and a method object, respectively. The value of MyClass. i can also be change by assignment. The attribute _doc_ is also a valid attribute, returning the docstring belonging to the class.

>>> type(MyClass)
 <type 'classobj'>

From the above expression, it can be noticed that MyClass is a class object.

Instantiation
A class object can be called to yield a class instance. Class instantiation uses function notation. For example (assuming the above class MyClass):

x=MyClass()

creates a new instance of the class MyClass, and assigns this object to the local variable x.

>>> type(MyClass ())
 <type 'instance'>
 >>> type(x)
 <<type 'instance's>

Many classes like to create objects with instances customized to a specific initial state. Therefore, a class may define a special method named init (), like this:

def _init_(self) :
 self.data=[ ]

When a class defines an _init_() method, class instantiation automatically invokes _init_() for the newly created class instance. So in this example, a new, initialized instance can be obtained by:

x=MyClass()

Of course, the _init_() method may have arguments for greater flexibility. In that case, arguments given to the class instantiation operator are passed on to _init_(). For example,

>>> class Complex:
 ... def _init_(self,realpart,imagpart):
 ... self.r=realpart
 ... self.i=imagpart
 ...
 >>> x=Complex(3.0, -4.5)
 >>> x.r,x.i
 (3.0, -4.5)

Instance object
The only operation that done using class instance object x is attribute references. There are two kinds of valid attribute names: “data attribute” and “method”.

Data attribute correspond to variable of a class instance. Data attributes need not be declared; like local variables, they spring into existence when they are first assigned to. For example, if x is the instance of MyClass (created before), the following piece of code will print the value 16, without leaving a trace:

>>> x.counter=1
 >>> while x.counter<10:
 ... x.counter=x.counter*2
 >>> print x.counter
 16
 >>> del x.counter

The other kind of instance attribute reference is a method. Any function object that is a class attribute defines a method for instances of that class. So, x. f is a valid method reference, since MyClass’. f is a function.

Method object
In the MyClass example, x . f is a method object, and x . f () returns the string ‘ hello world ‘. The call x. f () is exactly equivalent to MyClass . f (x). In general, calling a method with a list of n arguments is equivalent to calling the corresponding function with an argument list that is created by inserting the method’s object before the first argument

>>> x. f ()
 'hello world'
 >>> x.f()==MyClass.f(x)
 True

The method object can be stored and can be called later.

>>> xf=x.f
 >>> print xf()
 hello world
 >>> type(xf)
 <type 'instancemethod'>

Pre-defined attributes
Class and class instance objects has some pre-defined attributes:

Class object
Some pre-defined attributes of class object are:

_name_
This attribute give the class name.

>>> MyClass. _name_
 'MyClass'

_module_
This attribute give the module name in which the class was defined.

>>> MyClass. _module_
 ' main '

_dict_
A class has a namespace implemented by a dictionary object. Class attribute references are translated to lookups in this dictionary, e.g., MyClass . i is translated to MyClass ._dict_[ “i” ].

>>> MyClass._dict_
 { ' i ' : 12345, ' _module_ ': ' _main_ ', ' _doc_ ': 'A simple example
 class', 'f': <function f at 0x0640A070>}

_bases_
This attribute give the tuple (possibly empty or a singleton) containing the base classes.

>>> MyClass. _bases_

_doc_
This attribute give the class documentation string, or None, if not defined.

>>> MyClass. _doc_
 'A simple example class'

Class instance object
Some pre-defined attributes of class instance object are:

_dict_
This give attribute dictionary of class instance.

>>> x. dict
 { }

_class_
This give the instance’s class.

>>> x._class_
 <class_main_.MyClass at 0x063DA880>

Customizing attribute access
The following are some methods that can be defined to customize the meaning of attribute access for class instance.

object. _getattr_ (self,name)
Called when an attribute lookup does not find the attribute name. This method should return the (computed) attribute value or raise an AttributeError exception. Note that, if the attribute is found through the normal mechanism, _getattr_() is not called.

>>> class HiddenMembers:
 . . . def _getattr_ (self,name) :
 ... return "You don't get to see "+name
 >>> h=HiddenMembers()
 >>> h.anything
 "You don't get to see anything"

object. _setattr_ (self,name,value)
Called when an attribute assignment is attempted. The name is the attribute name and value is the
value to be assigned to it. Each class, of course, comes with a default _setattr_ , which simply set
the value of the variable, but that can be overridden.

>>> class Unchangable:
 ... def _setattr_ (self,name,value):
 ... print "Nice try"
 >>> u=Unchangable()
 >>> u.x=9
 Nice try
 >>> u.x
 Traceback (most recent call last):
 File "<stdin>", line 1, in ?
 AttributeError: Unchangable instance has no attribute 'x'

object. _delattr_ (self,name)
Like _setattr_ (), but for attribute deletion instead of assignment. This should only be
implemented if del ob j . name is meaningful for the object.

>>> class Permanent:
 ... def _delattr_ (self,name):
 ... print name,"cannot be deleted"
 >>> p=Permanent()
 >>> p.x=9
 >>> del p.x
 x cannot be deleted
 >>> p.x
 9

Class example
Till now, some basic concepts of class has been discussed. The following example “ClassExample.py” defines a class Person, which handles name and age of multiple individuals.

class Person:
 """The program handles individual's data"""
 population=0
 def _init_ (self,Name,Age):
 """Initializes the data."""
 self.name=Name
 self.age=Age
 Person.population+=1
 def _del_(self) :
 """Deleting the data."""
 print('Record of {0} is being removed'.format(self.name))
 Person.population-=1
 def AgeDetails(self):
 '''Age details:'''
 print('{0} is {1} years old'.format(self.name,self.age))
 def Records(els):
 """Print number of records."""
 print('There are {0} recordsformat(els.population))
 records=classmethod(Records)
 print Person. _doc_
 record1=Person('Ram', 26)
 print Person.AgeDetails. _doc_
 record1.AgeDetails()
 Person.records()
 record2=Person('Ahmed' , 20)
 print record2.AgeDetails. _doc_
 record2.AgeDetails()
 record2.records()
 record3=Person('John',22)
 print Person.AgeDetails. _doc_
 record3.AgeDetails()
 Person.records()
 del record1,record2
 Person.records()

The output is:

The program handles individual's data
 Age details:
 Ram is 26 years old
 There are 1 records
 Age details:
 Ahmed is 20 years old
 There are 2 records
 Age details:
 John is 22 years old *.
 There are 3 records
 Record of Ram is being removed
 Record of Ahmed is being removed
 There are 1 records

Variables defined in the class definition are class variables (population is a class variable); they are shared by all instances. To create instance variables (name and age are instance variables), they can be initialized in a method, e.g. self. name=value. Both class and instance variables are accessible through the notation self . name, and an instance variable hides a class variable with the same name when accessed in this way. Therefore, the class variable population is better referred as Person. population, and not self. population. The instance variables name and age are referred as self . name and self . age, respectively.

The Records is a method that belongs to the class and not to the instance. This is done by using classmethod () built-in function. A class method receives the class as implicit first argument, just like an instance method receives the instance. The class method can be called either on the class (Person . records ()) or on an instance (record2 . records ()). The instance is ignored except for its class.

The _doc_ attribute is used to access docstrings of class (Person. _doc_ ) and methods (record2 . AgeDetails . doc ).
The _del_() method is called when an instance is about to be destroyed. This is also called a destructor.

Inheritence
A class can be based on one or more other classes, called its “base class(es)”. It then inherits the data attributes and methods of its base classes. This is called “inheritance”, and the class which inherits from the base class is called “derived class”. A class definition first evaluates the inheritance list, if present. A simple form of derived class definition looks like:

class DerivedClassName(BaseClassName):
 <s.tatement-1>
 .
 .
 .
 <statement-N>

In place of a base class name BaseClassName, other arbitrary expressions are also allowed. This is useful, for example, when the base class is defined in another module:

class DerivedClassName(modname.BaseClassName):

The following example demonstrates class inheritance.

class Parent: # Base class definition
 parentAttr=100
 def _init_(self):
 print "Base class"
 def parentMethod(self):
 print 'Base class method'
 def setAttr(self, attr) :
 Parent.parentAttr=attr
 def getAttr(self):
 print "Parent attribute :",Parent.parentAttr
 class Child(Parent): # Derived class definition
 def _init_(self):
 print "Derived class"
 def childMethod(self):
 print 'Derived class method'
 c=Child()
 c.childMethod()
 c.parentMethod()
 c.setAttr(200)
 c.getAttr()

The output is:

Derived class
 Derived class method
 Base class method
 Parent attribute : 200

Execution of a derived class definition proceeds in the same way as for a base class. If a requested attribute is not found in the class, the search proceeds to look in the base class. This rule is applied recursively, if the base class itself is derived from some other class.
The following example is a step further in understanding inheritance.

class Person:
 population=0
 def _init_ (self,Name,Age):
 self.name=Name
 self.age=Age
 Person.population+=1
 def Record(self):
 print('Name:"{0}" Age:"{1}"'.format(self.name,self.age))
 class Employee(Person):
 def _init_ (self,Name,Age, Salary) :
 Person. _init_ (self,Name,Age)
 self.salary=Salary
 print('Entered record for {0format(self.name))
 def Record(self):
 Person.Record(self)
 print('Salary: "{0:d}"'.format(self.salary))
 class Employer(Person):
 def _init_ (self,Name,Age,Percentage):
 Person. _init_(self,Name, Age)
 self.percentage=Percentage
 print('Entered record for {0format(self.name))
 def Record(self):
 Person.Record(self)
 print('Partnership percent: "{0:d}format(self.percentage))
 employee1=Employee('Ram',26,25000)
 employee2=Employee('Ahmed',20,50000)
 employee3=Employee('John', 22,75000)
 employer1=Employer('Michael',58,60)
 employer2=Employer('Kishan',52,40)
 members= [employee1, employee2, employee3, employer1, employer2]
 for member in members:
 member.Record()

The output is:

Entered record for Ram
 Entered record for Ahmed
 Entered record for John
 Entered record for Michael
 Entered record for Kishan
 Name:"Ram" Age:"26"
 Salary: "25000"
 Name:"Ahmed" Age:"20"
 Salary: "50000"
 Name:"John" Age:"22"
 Salary: "75 000"
 Name:"Michael" Age:"58"
 Partnership percent: "60"
 Name:"Kishan" Age:"52"
 Partnership percent: "40"

Please note that, if a base class has an init () method, the derived class’s init ()
method, if any, must explicitly call it, to ensure proper initialization of the base class part of the instance; for example: BaseClass. init ( self, [args . . . ] ).

New-style and classic classes
Classes and instances come in two flavors: old-style (or classic) and new-style. New-style classes were introduced in Python 2.2. For compatibility reasons, classes are still old-style by default. Any class which inherits from object is a new-style class. The object class is a base for all new style classes. The following is an example showing simple definitions of old-style and new-style classes.

>>> class ClassicExample:
 ... def _init_ (self):
 ... pass
 >>> class NewStyleExample(object):
 ... def _init_ (self):
 ... pass
 >>>

Overriding Methods
Derived classes may override methods of their base classes. A method of a base class that calls another method defined in the same base class may end up calling a method of a derived class that overrides it.

# InheritenceExample2.py
 class Parent: # Base class definition
 def printInfo(self):
 print 'Base class method'
 def parentMethod(self):
 self.printInfo()
 class Child(Parent): # Derived class definition
 def printlnfo(self):
 print 'Derived class method'
 c=Child()
 c.parentMethod()

The output is:

Derived class method

It can be seen that print Inf o () of derived class is called instead of base class.

Super() function
The is a built-in function super (), which can be used for accessing inherited methods that have been overridden in a class. The super () only works for new-style classes; in a class hierarchy with single inheritance, super can be used to refer to parent classes without naming them explicitly, thus making the code more maintainable.

class Parent(object): # Base class definition
 def printlnfo(self):
 print 'Base class method'
 def parentMethod(self) :
 self.printInfo()
 class Child(Parent): # Derived class definition
 def printInfo(self):
 super(Child,self).printlnfo()
 # Parent.printInfo(self)
 print 'Derived class method'
 c=Child()
 c.parentMethod()

The output is:

Base class method
 Derived class method

In the above example, to access the printlnfo () method of Parent class, super () method in the form of super (Child, self) .printlnfo () is used, where the name of base class is not mentioned. The other way would have been by using Parent .printlnfo (self).

Name mangling
In Python, there is a mechanism called “name mangling” to avoid name clashes of names in class with
names defined by sub-classes. Any identifier of the form spam (at least two leading underscores, at
most one trailing underscore) is textually replaced with _classname. spam, where classname is
the current class name. Note that, the mangling rule is designed mostly to avoid accidents.

# InheritenceExample3.py
 class Parent: # Base class definition
 def _printlnfo(self):
 print 'Base class method'
 def parentMethod(self):
 self. _printlnfo()
 class Child(Parent): # Derived class definition
 def _printlnfo(self):
 print 'Derived class method'
 c=Child()
 print Parent._diet_.keys()
 print Child. _dict_ .keys()
 c.parentMethod()
 c._Child_printlnfo()
 c._Parent_printlnfo()

The output is:

[' _module_ ', '_Parent _printlnfo', ' _doc_ 'parentMethod']
 [' _module_ ' _doc_', '_Child_printlnfo']
 Base class method
 Derived class method
 Base class method

Multiple inheritence
Till now, the discussion was about inheriting from one class; this is called “single inheritance”. Python also supports “multiple inheritance”, where a class can inherit from more than one class. A simple class definition inheriting from multiple base classes looks like:

class DerivedClassName(Base1, Base2, Base3):
 <statement-1>
 .
 .
 .
 <statement-N>

Whenever there is a call via DerivedClassName class instance, Python has to look-up the possible
function in the class hierarchy for Basel, Base2 , Base3 , but it needs to do this in a consistent
order. To do this, Python uses an approach called “method resolution order” (MRO) using an algorithm called “C3” to get it straight.
Consider the following multiple inheritance example.

class A(object):
 def printInfo ( self) :
 print 'Class A'
 class B(A):
 def printInfo(self):
 print 'Class B'
 # super(B,self).printInfo()
 A.printInfo(self)
 class C(A):
 def printlnfo(self):
 print 'Class C'
 . super(C,self).printlnfo()
 class D(B,C):
 def printInfo (self) :
 print 'Class D'
 super(D,self).printlnfo()
 foo=D ( )
 foo.printInfo ()

Running the code yield the following output.

Class D
 Class B
 Class A

It can be observed that C class printInfo() is skipped. The reason for that is because B class printlnfoO calls A class printlnfoO directly. The purpose of super () is to entertain method resolution order. Now un-comment super(B,self) .printlnfoO and comment-out A. print Inf o (self). The code now yields a different result.

Class D
 Class B
 Class C
 Class A

Now all the printlnfo() methods get called. Notice that at the time of defining B .printlnfo (), one can think that super (B, self) .printlnfoO is the same as calling A.printlnfo (self), however, this is wrong. In the above situation,
super (B, self) .printlnfoO actually calls C . print Inf o ( self).

Python Programming FundamentalsComputer Science