OOP(object-oriented programming) in python

Introduction

OOP is an approach for modeling concrete, real-world things as well as relations between them. It allows programmers to create their own real-world data types and it is based on classes, objects, attributes, and methods. In object-oriented programming, we write classes that represent real-world things and situations, and we create objects based on these classes.

A class is a blueprint or a factory for creating objects and in python, everything is based on an object. Classes provide a means of bundling data and functionality together. An object is a unique instance of a data structure defined by its class. An Instance is an individual object of a certain class. The creation of an instance of the class is called instantiation.

The data values which we store inside an object are called attributes and are of two types:

  • Class attributes and
  • Instance attributes. 

Class attributes are a variable that is defined outside the methods at the class level which are shared by all instances. The data value will be the same for all the objects but an instance attribute can overwrite it. To access the class attribute, we access it by ClassName.attributeName or objectName.attributeName

Instance attributes is a unique data or variables that are declared inside the method. It is created in the constructor (__init__( ) method) or in other instance methods. We access instance attributes using objectName.attributeName

The functions which are associated with the object are called methods or a function that you define inside the class is called a method.



How to create a class

Class definitions, like function definitions (def statements), must be executed before they have any effect. To create a class, use the keyword class.

Syntax to create a class:

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

The statements inside a class definition will usually be function definitions, but other statements are also allowed.

Class object

Use the class name to create an object.

Example:

class ClassName:    # Define a class
    x = 10          # Declare a class variable
obj = ClassName()   # Creating an object 
print(obj.x)        # Object references class attribute

# Output:
10

Class objects support two kinds of operations:

  • attribute references and
  • instantiation

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.

Example:

class ClassName:   # Define a class
    x = 10    
obj = ClassName()  # Create an object 
print(obj.x)       # Attribute reference with object name

# Output:
10

The instantiation operation (“calling” a class object) creates an empty object. Many classes like to create objects with instances customized to a specific initial state. Therefore a class may define a special method named __init__().

__init__ method:

The __init__ method is often referred to as the “constructor”, since it is responsible for constructing new instances. All classes have a function called __init__(), which is always executed when the class is being initiated. It is used to assign values to object properties or other operations that are necessary to do when the object is being created.

Example:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print("Name of the person:",self.name)
        print("Age of the person:",self.age)
Person("Dawa", 25)

Output:

Name of the person: Dawa
Age of the person: 25


Instance Objects

The only operations understood by instance objects are attribute references. There are two kinds of valid attribute names; data attributes, and methods.  

Data attributes correspond to “instance variables”. Data attributes need not be declared; like local variables, they spring into existence when they are first assigned.

The other kind of instance attribute reference is a method. A method is a function that “belongs to” an object.

Example:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print("Name: ", self.name, "\nAge:", self.age)
    def address(self, village, gewog, dzongkhag):
        self.village = village
        self.gewog = gewog
        self.dzongkhag = dzongkhag
        print("Village: ", self.village, "\nGewog: ", self.gewog)

p = Person( 'Sonam', 25)
p.address('kikher','nangkhor', 'Zhemgang')  # Method attribute reference
print("Dzongkhag:", p.dzongkhag)            # Data attribute reference

Output:

Name:  Sonam 
Age: 25
Village:  kikher 
Gewog:  nangkhor
Dzongkhag: Zhemgang

Note: The first argument of a method is called self. This is nothing more than a convention: the name self has absolutely no special meaning to Python. However, by not following the convention your code may be less readable to other Python programmers, and it is also conceivable that a class browser program might be written that relies upon such a convention.

Class variable and Instance variable

Instance variables are for unique data to each instance and class variables are for attributes and methods shared by all class instances.

Example:

class Person:
    location = "East"         # Class variable shared by all instances
    def __init__(self, name):
        self.name = name      # Instance variable unique to each instance
p = Person('Dawa')
q = Person('Sonam')
print("Shared by all instances:", p.location)
print("Shared by all instances:", q.location)
print("Unique to each instances:", p.name)
print("Unique to each instances:", q.name)

Output:

Shared by all instances: East
Shared by all instances: East
Unique to each instances: Dawa
Unique to each instances: Sonam

If the same attribute name occurs in both an instance and a class, then attribute lookup prioritizes the instance.

Example:

class Person:
    location = "East"            # Class variable shared by all instances
    def __init__(self,location):
        self.location = location # Instance variable unique to each instance
p = Person("West")
print(p.location)

Output:

West

The function definition doesn’t need to be textually enclosed in the class definition: assigning a function object to a local variable in the class will also work.

Example:

def f1(self):
    return 'Hello world'
class Person:
    f = f1   # Assigning a function object to a local variable in the class
    location = "East" 
    def __init__(self,location):
        self.location = location 
p = Person("West")
print(p.location)
print(p.f()) # Accessing a local variable of the class

Output:

West
Hello world


Class inheritance

The class inheritance mechanism allows multiple base classes. A derived (child) class can override any methods of its base (Parent) class or classes, and a method can call the method of a base class with the same name.

Execution of a derived class definition proceeds the same as for a base class. When the derived class object is constructed, the base class is remembered. This is used for resolving attribute references: if a requested attribute is not found in the derived 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.

Example:

class Animal:               # Base class
    def __init__(self, name, color):
        self.name = name
        self.color = color
    def eat(self):
        print(self.name, "is eating.")
class Dog(Animal):          # Derived class inherited Base class
    def bite(self):
        print(self.name,"will bite.")

dog = Dog("Katu", "black")  # Derived class object created
dog.eat()                   # Base class method is called
dog.bite()                  # Derived class method is called

Output:

Katu is eating.
Katu will bite.
Multiple Inheritance

Python supports a form of multiple inheritances as well. A class definition with multiple base classes looks like this:

Syntax:

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

If an attribute is not found in DerivedClassName, it is searched for in Base1, then (recursively) in the base classes of Base1, and if it was not found there, it was searched for in Base2, and so on.

Overriding methods: Polymorphism

When a method name in the child class is the same as the method name in the parent class, it will execute the child method instead of the parent method. This is known as the “overriding parent method” also known as polymorphism.

Example 1:

class BaseClass:
    def f1(self):
        print("Hello world from Base Class")
class DerivedClass(BaseClass):
    def f1(self):
        print("Hello World from Derived Class")
d = DerivedClass()
d.f1()

Output:

Hello World from Derived Class

We can see that the method name f1() is the same in the derived class as well as in the base class. However, the program has executed the method in the derived class instead of the base class.

Example 2:

class Shape:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def area(self):
        print("Area of shape:", self.width*self.height)
class Rectangle(Shape):
    def area(self):
        print("Area of Rectangle:", self.width*self.height)
class Triangle(Shape):
    def area(self):
        print("Area of Rectangle:", (self.width*self.height)/2)

r = Rectangle(10, 5)
t = Triangle(5, 15)
r.area()
t.area() 

Output:

Area of Rectangle: 50
Area of Rectangle: 37.5


Iterators and Generators

An iterator is an object which allows a programmer to loop through all the elements of the collection like a list, tuple, dictionary, etc….

It implements the iterators protocol and consists of two methods i.e. __iter__() and __next__() which also has a built-in function iter() and next() respectively. The use of iterators pervades and unifies Python. Behind the scenes, the for statement calls iter() on the container object. The function returns an iterator object that defines the method __next__() which accesses elements in the container one at a time. When there are no more elements, __next__() raises a StopIteration exception which tells the for loop to terminate.

Example:

animal = ['cat', 'dog']
n = iter(animal)
print(n.__next__())
print(n.__next__())
print(n.__next__())

Output:

cat
dog
Traceback (most recent call last):
  File "E:/New folder/Bhutan Python coders/code for blog/class and instanes.py", line 6, in <module>
    print(n.__next__())
StopIteration

It is easy to add iterator behavior to your classes. Define an __iter__() method which returns an object with a __next__() method. If the class defines __next__(), then __iter__() can just return self.

Example:

class IterClass:
    def __init__(self):
        pass
    def __iter__(self):
        self.count = 0
        return self
    def __next__(self):
        count = self.count
        self.count+=1
        return count
itr = IterClass()
i = iter(itr)
print(i.__next__())
print(i.__next__())
print(i.__next__())

Output:

0
1
2

Generators are a simple and powerful tool for creating iterators. They are written like regular functions but use the yield statement whenever they want to return data.

Example:

def gen():
    name = 'Dawa'
    age = '25'
    yield name
    yield age
g = gen()
print(g.__next__())
print(g.__next__())

Output:

Dawa
25