Category: Object Oriented Programming

  •  Reflection

    In object-oriented programming, reflection refers to the ability to extract information about any object in use. You can get to know the type of object, whether is it a subclass of any other class, what are its attributes, and much more. Python’s standard library has several functions that reflect on different properties of an object. Reflection is also sometimes called introspect.

    Following is the list of reflection functions in Python −

    • type() Function
    • isinstance() Function
    • issubclass() Function
    • callable() Function
    • getattr() Function
    • setattr() Function
    • hasattr() Function
    • dir() Function

    The type() Function

    We have used this function many times. It tells you which class an object belongs to.

    Example

    Following statements print the respective class of different built-in data type objects

    print(type(10))print(type(2.56))print(type(2+3j))print(type("Hello World"))print(type([1,2,3]))print(type({1:'one',2:'two'}))

    Here, you will get the following output −

    <class 'int'>
    <class 'float'>
    <class 'complex'>
    <class 'str'>
    <class 'list'>
    <class 'dict'>
    

    Let us verify the type of an object of a user-defined class −

    classtest:pass
       
    obj = test()print(type(obj))

    It will produce the following output −

    <class '__main__.test'>

    The isinstance() Function

    This is another built-in function in Python which ascertains if an object is an instance of the given class.

    Syntax

    isinstance(obj,class)

    This function always returns a Boolean value, true if the object is indeed belongs to the given class and false if not.

    Example

    Following statements return True −

    print(isinstance(10,int))print(isinstance(2.56,float))print(isinstance(2+3j,complex))print(isinstance("Hello World",str))

    It will produce the following output −

    True
    True
    True
    True
    

    In contrast, these statements print False.

    print(isinstance([1,2,3],tuple))print(isinstance({1:'one',2:'two'},set))

    It will produce the following output −

    False
    False
    

    You can also perform check with a user defined class

    classtest:pass
       
    obj = test()print(isinstance(obj, test))

    It will produce the following output −

    True
    

    In Python, even the classes are objects. All classes are objects of object class. It can be verified by following code −

    classtest:passprint(isinstance(int,object))print(isinstance(str,object))print(isinstance(test,object))

    All the above print statements print True.

    The issubclass() Function

    This function checks whether a class is a subclass of another class. Pertains to classes, not their instances.

    As mentioned earlier, all Python classes are subclassed from object class. Hence, output of following print statements is True for all.

    classtest:passprint(issubclass(int,object))print(issubclass(str,object))print(issubclass(test,object))

    It will produce the following output −

    True
    True
    True
    

    The callable() Function

    An object is callable if it invokes a certain process. A Python function, which performs a certain process, is a callable object. Hence callable(function) returns True. Any function, built-in, user-defined, or method is callable. Objects of built-in data types such as int, str, etc., are not callable.

    Example

    deftest():passprint(callable("Hello"))print(callable(abs))print(callable(list.clear([1,2])))print(callable(test))

    string object is not callable. But abs is a function which is callable. The pop method of list is callable, but clear() is actually call to the function and not a function object, hence not a callable

    It will produce the following output −

    False
    True
    True
    False
    True
    

    A class instance is callable if it has a __call__() method. In the example below, the test class includes __call__() method. Hence, its object can be used as if we are calling function. Hence, object of a class with __call__() function is a callable.

    classtest:def__init__(self):passdef__call__(self):print("Hello")
    
      
    obj = test() obj()print("obj is callable?",callable(obj))

    It will produce the following output −

    Hello
    obj is callable? True
    

    The getattr() Function

    The getattr() built-in function retrieves the value of the named attribute of object.

    Example

    classtest:def__init__(self):
    
      self.name ="Manav"
      
    obj = test()print(getattr(obj,"name"))

    It will produce the following output −

    Manav
    

    The setattr() Function

    The setattr() built-in function adds a new attribute to the object and assigns it a value. It can also change the value of an existing attribute.

    In the example below, the object of test class has a single attribute − name. We use setattr() to add age attribute and to modify the value of name attribute.

    classtest:def__init__(self):
    
      self.name ="Manav"
      
    obj = test()setattr(obj,"age",20)setattr(obj,"name","Madhav")print(obj.name, obj.age)

    It will produce the following output −

    Madhav 20
    

    The hasattr() Function

    This built-in function returns True if the given attribute is available to the object argument, and false if not. We use the same test class and check if it has a certain attribute or not.

    classtest:def__init__(self):
    
      self.name ="Manav"
      
    obj = test()print(hasattr(obj,"age"))print(hasattr(obj,"name"))

    It will produce the following output −

    False
    True
    

    The dir() Function

    If this built-in function is called without an argument, return the names in the current scope. For any object as an argument, it returns a list of the attributes of the given object and attributes reachable from it.

    • For a module object − the function returns the module’s attributes.
    • For a class object − the function returns its attributes, and recursively the attributes of its bases.
    • For any other object − its attributes, its class’s attributes, and recursively the attributes of its class’s base classes.

    Example

    print("dir(int):",dir(int))

    It will produce the following output −

    dir(int): ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
    

    Example

    print("dir(dict):",dir(dict))

    It will produce the following output −

    dir(dict): ['__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__ior__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__ror__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
    

    Example

    classtest:def__init__(self):
    
      self.name ="Manav"
    obj = test()print("dir(obj):",dir(obj))

    It will produce the following output −

    dir(obj): ['__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__', 'name']
  •  Enums

    Enums in Python

    In Python, the term enumeration refers to the process of assigning fixed constant values to a set of strings so that each string can be identified by the value bound to it. The Enum class included in enum module (which is a part of Python’s standard library) is used as the parent class to define enumeration of a set of identifiers − conventionally written in upper case.

    Example

    In the below code, “subjects” is the enumeration. It has different enumeration members and each member is an object of the enumeration class subjects. These members have name and value attributes.

    # importing enum from enum import Enum
    
    classsubjects(Enum):
       ENGLISH =1
       MATHS =2
       SCIENCE =3
       SANSKRIT =4
    
    obj = subjects.MATHS
    print(type(obj))

    It results in following output −

    <enum 'subjects'> 
    

    An enum class cannot have the same member appearing twice, however, more than one member may be assigned the same value. To ensure that each member has a unique value bound to it, use the @unique decorator.

    Example

    In this example, we are using the @unique decorator to restrict duplicacy.

    from enum import Enum, unique
    
    @uniqueclasssubjects(Enum):
       ENGLISH =1
       MATHS =2
       GEOGRAPHY =3
       SANSKRIT =2

    This will raise an exception as shown below −

       @unique
    
    ^^^^^^
    raise ValueError('duplicate values found in %r: %s' % ValueError: duplicate values found in <enum 'subjects'>: SANSKRIT -> MATHS

    The Enum class is a callable class, hence you can use its constructor to create an enumeration. This constructor accepts two arguments, which are the name of enumeration and a string consisting of enumeration member symbolic names separated by a whitespace.

    Example

    following is an alternative method of defining an enumeration −

    from enum import Enum
    subjects = Enum("subjects","ENGLISH MATHS SCIENCE SANSKRIT")print(subjects.ENGLISH)print(subjects.MATHS)print(subjects.SCIENCE)print(subjects.SANSKRIT)

    This code will give the following output −

    subjects.ENGLISH
    subjects.MATHS
    subjects.SCIENCE
    subjects.SANSKRIT

    Accessing Modes in Enums

    Members of an enum class can be accessed in two modes −

    Value − In this mode, value of the enum member is accessed using the "value" keyword followed by object of the enum class.
    Name − Similarly, we use the "name" keyword to access name of the enum member.
    Example

    The following example illustrates how to access value and name of the enum member.

    from enum import Enum

    class subjects(Enum):
    ENGLISH = "E"
    MATHS = "M"
    GEOGRAPHY = "G"
    SANSKRIT = "S"

    obj = subjects.SANSKRIT
    print(type(obj))
    print(obj.name)
    print(obj.value)
    It will produce the following output −

    <enum 'subjects'>
    SANSKRIT
    S
    Iterating through Enums

    You can iterate through the enum members in the order of their appearance in the definition, with the help of a for loop.

    Example

    The following example shows how to iterate through an enumeration using for loop −

    from enum import Enum

    class subjects(Enum):
    ENGLISH = "E"
    MATHS = "M"
    GEOGRAPHY = "G"
    SANSKRIT = "S"

    for sub in subjects:
    print (sub.name, sub.value)
    It will produce the following output −

    ENGLISH E
    MATHS M
    GEOGRAPHY G
    SANSKRIT S
    We know that enum member can be accessed with the unique value assigned to it, or by its name attribute. Hence, subjects("E") as well as subjects["ENGLISH"] returns subjects.ENGLISH member.

  • Wrapper Classes

    function in Python is a first-order object. A function can have another function as its argument and wrap another function definition inside it. This helps in modifying a function without actually changing it. Such functions are called decorators.

    This feature is also available for wrapping a class. This technique is used to manage the class after it is instantiated by wrapping its logic inside a decorator.

    Example

    defdecorator_function(Wrapped):classWrapper:def__init__(self,x):
    
         self.wrap = Wrapped(x)defprint_name(self):return self.wrap.name
    return Wrapper @decorator_functionclassWrapped:def__init__(self,x):
      self.name = x
      
    obj = Wrapped('TutorialsPoint')print(obj.print_name())

    Here, Wrapped is the name of the class to be wrapped. It is passed as argument to a function. Inside the function, we have a Wrapper class, modify its behavior with the attributes of the passed class, and return the modified class. The returned class is instantiated and its method can now be called.

    When you execute this code, it will produce the following output −

    TutorialsPoint
  •  Singleton Class

    In Python, a Singleton class is the implementation of singleton design pattern which means this type of class can have only one object. This helps in optimizing memory usage when you perform some heavy operation, like creating a database connection.

    If we try to create multiple objects for a singleton class, the object will be created only for the first time. After that, the same object instance will be returned.

    Creating Singleton Classes in Python

    We can create and implement singleton classes in Python using the following ways −

    • using __init__
    • using __new__

    Using __init__

    The __init__ method is an instance method that is used for initializing a newly created object. Its automatically called when an object is created from a class.

    If we use this method with a static method and provide necessary checks i.e., whether an instance of the class already exists or not, we can restrict the creation of a new object after the first one is created. 

    Example

    In the below example, we are creating a singleton class using the __init__ method.

    classSingleton:
      __uniqueInstance =None@staticmethoddefcreateInstance():if Singleton.__uniqueInstance ==None:
    
      Singleton()return Singleton.__uniqueInstance
    
    def__init__(self):if Singleton.__uniqueInstance !=None:raise Exception("Object exist!")else:
          Singleton.__uniqueInstance = self
           
    obj1 = Singleton.createInstance()print(obj1) obj2 = Singleton.createInstance()print(obj2)

    When we run the above code, it will show the following result −

    <__main__.Singleton object at 0x7e4da068a910>
    <__main__.Singleton object at 0x7e4da068a910>
    

    Using __new__

    The __new__ method is a special static method in Python that is called to create a new instance of a class. It takes the class itself as the first argument and returns a new instance of that class.

    When an instance of a Python class is declared, it internally calls the __new__() method. If you want to implement a Singleton class, you can override this method. 

    In the overridden method, you first check whether an instance of the class already exists. If it doesnt (i.e., if the instance is None), you call the super() method to create a new object. At the end, save this instance in a class attribute and return the result.

    Example

    In the following example, we are creating a singleton class using the __new__ method.

    classSingletonClass:
       _instance =Nonedef__new__(cls):if cls._instance isNone:print('Creating the object')
    
         cls._instance =super(SingletonClass, cls).__new__(cls)return cls._instance
      
    obj1 = SingletonClass()print(obj1) obj2 = SingletonClass()print(obj2)

    The above code gives the following result −

    Creating the object
    <__main__.SingletonClass object at 0x000002A5293A6B50>
    <__main__.SingletonClass object at 0x000002A5293A6B50>
  •  Anonymous Class and Objects

    Python’s built-in type() function returns the class that an object belongs to. In Python, a class, both a built-in class or a user-defined class are objects of type class.

    Example

    Open Compiler

    classmyclass:def__init__(self):
    
      self.myvar=10return
      
    obj = myclass()print('class of int',type(int))print('class of list',type(list))print('class of dict',type(dict))print('class of myclass',type(myclass))print('class of obj',type(obj))

    It will produce the following output −

    class of int <class 'type'>
    class of list <class 'type'>
    class of dict <class 'type'>
    class of myclass <class 'type'>
    

    The type() has a three argument version as follows −

    Syntax

    newclass=type(name, bases, dict)
    

    Using above syntax, a class can be dynamically created. Three arguments of type function are −

    • name − name of the class which becomes __name__ attribute of new class
    • bases − tuple consisting of parent classes. Can be blank if not a derived class
    • dict − dictionary forming namespace of the new class containing attributes and methods and their values.

    Create an Anonymous Class

    We can create an anonymous class with the above version of type() function. The name argument is a null string, second argument is a tuple of one class the object class (note that each class in Python is inherited from object class). We add certain instance variables as the third argument dictionary. We keep it empty for now.

    anon=type('',(object,),{})
    
    Create an Anonymous Object
    
    To create an object of this anonymous class −
    
    obj = anon()
    print ("type of obj:", type(obj))
    The result shows that the object is of anonymous class
    
    type of obj: <class '__main__.'>
    Anonymous Class and Object Example
    
    We can also add instance variables and instance methods dynamically. Take a look at this example −
    
    def getA(self):
       return self.a
    obj = type('',(object,),{'a':5,'b':6,'c':7,'getA':getA,'getB':lambda self : self.b})()
    print (obj.getA(), obj.getB())
    It will produce the following output −
    
    5 6

  • Inner Classes

    Inner Class in Python

    A class defined inside another class is known as an inner class in Python. Sometimes inner class is also called nested class. If the inner class is instantiated, the object of inner class can also be used by the parent class. Object of inner class becomes one of the attributes of the outer class. Inner class automatically inherits the attributes of the outer class without formally establishing inheritance.

    Syntax

    classouter:def__init__(self):passclassinner:def__init__(self):pass

    An inner class lets you group classes. One of the advantages of nesting classes is that it becomes easy to understand which classes are related. The inner class has a local scope. It acts as one of the attributes of the outer class.

    Example

    In the following code, we have student as the outer class and subjects as the inner class. The __init__() constructor of student initializes name attribute and an instance of subjects class. On the other hand, the constructor of inner subjects class initializes two instance variables sub1, sub2.

    A show() method of outer class calls the method of inner class with the object that has been instantiated.

    classstudent:def__init__(self):
    
      self.name ="Ashish"
      self.subs = self.subjects()returndefshow(self):print("Name:", self.name)
      self.subs.display()classsubjects:def__init__(self):
         self.sub1 ="Phy"
         self.sub2 ="Che"returndefdisplay(self):print("Subjects:",self.sub1, self.sub2)
         
    s1 = student() s1.show()

    When you execute this code, it will produce the following output −

    Name: Ashish
    Subjects: Phy Che
    

    It is quite possible to declare an object of outer class independently, and make it call its own display() method.

    sub = student().subjects().display()

    It will list out the subjects.

    Types of Inner Class

    In Python, inner classes are of two types −

    • Multiple Inner Class
    • Multilevel Inner Class

    Multiple Inner Class

    In multiple inner class, a single outer class contains more than one inner class. Each inner class works independently but it can interact with the members of outer class.

    Example

    In the below example, we have created an outer class named Organization and two inner classes.

    classOrganization:def__init__(self):
    
      self.inner1 = self.Department1()
      self.inner2 = self.Department2()defshowName(self):print("Organization Name: Tutorials Point")classDepartment1:defdisplayDepartment1(self):print("In Department 1")classDepartment2:defdisplayDepartment2(self):print("In Department 2")# instance of OuterClass
    outer = Organization()# Calling show method outer.showName()# InnerClass instance 1 inner1 = outer.inner1 # Calling display method inner1.displayDepartment1()# InnerClass instance 2 inner2 = outer.inner2 # Calling display method inner2.displayDepartment2()

    On executing, this code will produce the following output −

    Organization Name: Tutorials Point
    In Department 1
    In Department 2
    

    Multilevel Inner Class

    It refers to an inner class that itself contains another inner class. It creates multiple levels of nested classes.

    Example

    The following code explains the working of Multilevel Inner Class in Python −

    classOrganization:def__init__(self):
    
      self.inner = self.Department()defshowName(self):print("Organization Name: Tutorials Point")classDepartment:def__init__(self):
         self.innerTeam = self.Team1()defdisplayDep(self):print("In Department")classTeam1:defdisplayTeam(self):print("Team 1 of the department")# instance of outer class                
    outer = Organization()# call the method of outer class outer.showName()# Inner Class instance inner = outer.inner inner.displayDep()# Access Team1 instance innerTeam = inner.innerTeam # Calling display method innerTeam.displayTeam()

    When you run the above code, it will produce the below output −

    Organization Name: Tutorials Point
    In Department
    Team 1 of the department
  • Packages

    In Python, the module is a Python script with a .py extension and contains objects such as classes, functions, etc. Packages in Python extend the concept of the modular approach further. The package is a folder containing one or more module files; additionally, a special file “__init__.py” file may be empty but may contain the package list.

    Create a Python Package

    Let us create a Python package with the name mypackage. Follow the steps given below −

    • Create an outer folder to hold the contents of mypackage. Let its name be packagedemo.
    • Inside it, create another folder mypackage. This will be the Python package we are going to construct. Two Python modules areafunctions.py and mathfunctions.py will be created inside mypackage.
    • Create an empty “__.init__.py” file inside mypackage folder.
    • Inside the outer folder, we shall later store a Python script example.py to test our package.

    The file/folder structure should be as shown below −

    folder_structure

    Using your favorite code editor, save the following two Python modules in mypackage folder.

    Example to Create a Python Package

    # mathfunctions.pydefsum(x,y):
       val = x+y
       return val
       
    defaverage(x,y):
       val =(x+y)/2return val
    
    defpower(x,y):
       val = x**y
       return val
    

    Create another Python script −

    # areafunctions.pydefrectangle(w,h):
       area = w*h
       return area
       
    defcircle(r):import math
       area = math.pi*math.pow(r,2)return area
    

    Let us now test the myexample package with the help of a Python script above this package folder. Refer to the folder structure above.

    #example.pyfrom mypackage.areafunctions import rectangle
    print("Area :", rectangle(10,20))from mypackage.mathsfunctions import average
    print("average:", average(10,20))

    This program imports functions from mypackage. If the above script is executed, you should get following output −

    Area : 200
    average: 15.0
    Define Package List

    You can put selected functions or any other resources from the package in the "__init__.py" file. Let us put the following code in it.

    from .areafunctions import circle
    from .mathsfunctions import sum, power
    To import the available functions from this package, save the following script as testpackage.py, above the package folder as before.

    Example to Define a Package List

    #testpackage.py
    from mypackage import power, circle

    print ("Area of circle:", circle(5))
    print ("10 raised to 2:", power(10,2))
    It will produce the following output −

    Area of circle: 78.53981633974483
    10 raised to 2: 100
    Package Installation

    Right now, we are able to access the package resources from a script just above the package folder. To be able to use the package anywhere in the file system, you need to install it using the PIP utility.

    First of all, save the following script in the parent folder, at the level of package folder.

    #setup.py
    from setuptools import setup
    setup(name='mypackage',
    version='0.1',
    description='Package setup script',
    url='#',
    author='anonymous',
    author_email='[email protected]',
    license='MIT',
    packages=['mypackage'],
    zip_safe=False)
    Run the PIP utility from command prompt, while remaining in the parent folder.

    C:\Users\user\packagedemo>pip3 install .
    Processing c:\users\user\packagedemo
    Preparing metadata (setup.py) ... done
    Installing collected packages: mypackage
    Running setup.py install for mypackage ... done
    Successfully installed mypackage-0.1
    You should now be able to import the contents of the package in any environment.

    C:\Users>python
    Python 3.11.2 (tags/v3.11.2:878ead1, Feb 7 2023, 16:38:35) [MSC v.1934 64 bit (AMD64)] on win32
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import mypackage
    >>> mypackage.circle(5)
    78.53981633974483
  •  Interfaces

    In software engineering, an interface is a software architectural pattern. It is similar to a class but its methods just have prototype signature definition without any executable code or implementation body. The required functionality must be implemented by the methods of any class that inherits the interface.

    The method defined without any executable code is known as abstract method.

    Interfaces in Python

    In languages like Java and Go, there is keyword called interface which is used to define an interface. Python doesn’t have it or any similar keyword. It uses abstract base classes (in short ABC module) and @abstractmethod decorator to create interfaces. 

    NOTE: In Python, abstract classes are also created using ABC module.

    An abstract class and interface appear similar in Python. The only difference in two is that the abstract class may have some non-abstract methods, while all methods in interface must be abstract, and the implementing class must override all the abstract methods.

    Rules for implementing Python Interfaces

    We need to consider the following points while creating and implementing interfaces in Python −

    • Methods defined inside an interface must be abstract. 
    • Creating object of an interface is not allowed.
    • A class implementing an interface needs to define all the methods of that interface.
    • In case, a class is not implementing all the methods defined inside the interface, the class must be declared abstract. 

    Ways to implement Interfaces in Python

    We can create and implement interfaces in two ways −

    • Formal Interface
    • Informal Interface

    Formal Interface

    Formal interfaces in Python are implemented using abstract base class (ABC). To use this class, you need to import it from the abc module.

    Example

    In this example, we are creating a formal interface with two abstract methods.

    from abc import ABC, abstractmethod
    
    # creating interfaceclassdemoInterface(ABC):@abstractmethoddefmethod1(self):print("Abstract method1")return@abstractmethoddefmethod2(self):print("Abstract method1")return

    Let us provide a class that implements both the abstract methods.

    # class implementing the above interfaceclassconcreteclass(demoInterface):defmethod1(self):print("This is method1")returndefmethod2(self):print("This is method2")return# creating instance      
    obj = concreteclass()# method call
    obj.method1()
    obj.method2()

    Output

    When you execute this code, it will produce the following output −

    This is method1
    This is method2
    

    Informal Interface

    In Python, the informal interface refers to a class with methods that can be overridden. However, the compiler cannot strictly enforce the implementation of all the provided methods. 

    This type of interface works on the principle of duck typing. It allows us to call any method on an object without checking its type, as long as the method exists.

    Example

    In the below example, we are demonstrating the concept of informal interface.

    classdemoInterface:defdisplayMsg(self):passclassnewClass(demoInterface):defdisplayMsg(self):print("This is my message")# creating instance      
    obj = newClass()# method call
    obj.displayMsg()

    Output

    On running the above code, it will produce the following output −

    This is my message
    
  • Encapsulation

    Encapsulation is the process of bundling attributes and methods within a single unit. It is one of the main pillars on which the object-oriented programmingparadigm is based.

    We know that a class is a user-defined prototype for an object. It defines a set of data members and methods, capable of processing the data.

    According to the principle of data encapsulation, the data members that describe an object are hidden from the environment external to the class. They can only be accessed through the methods within the same class. Methods themselves on the other hand are accessible from outside class context. Hence, object data is said to be encapsulated by the methods. In this way, encapsulation prevents direct access to the object data.

    Implementing Encapsulation in Python

    Languages such as C++ and Java use access modifiers to restrict access to class members (i.e., variables and methods). These languages have keywords public, protected, and private to specify the type of access.

    A class member is said to be public if it can be accessed from anywhere in the program. Private members are allowed to be accessed from within the class only. Usually, methods are defined as public, and instance variables are private. This arrangement of private instance variables and public methods ensures the implementation of encapsulation.

    Unlike these languages, Python has no provision to specify the type of access that a class member may have. By default, all the variables and methods in a Python class are public, as demonstrated by the following example.

    Example 1

    Here, we have an Employee class with instance variables, name and age. An object of this class has these two attributes. They can be directly accessed from outside the class, because they are public.

    classStudent:def__init__(self, name="Rajaram", marks=50):
    
      self.name = name
      self.marks = marks
    s1 = Student() s2 = Student("Bharat",25)print("Name: {} marks: {}".format(s1.name, s2.marks))print("Name: {} marks: {}".format(s2.name, s2.marks))

    It will produce the following output −

    Name: Rajaram marks: 50
    Name: Bharat marks: 25
    

    In the above example, the instance variables are initialized inside the class. However, there is no restriction on accessing the value of instance variables from outside the class, which is against the principle of encapsulation.

    Although there are no keywords to enforce visibility, Python has a convention of naming the instance variables in a peculiar way. In Python, prefixing name of a variable/method with a single or double underscore to emulate the behavior of protected and private access modifiers.

    If a variable is prefixed by a single double underscore (such as “__age“), the instance variable is private, similarly if a variable name is prefixed with a single underscore (such as “_salary“)

    Example 2

    Let us modify the Student class. Add another instance variable salary. Make name private and marks as private by prefixing double underscores to them.

    classStudent:def__init__(self, name="Rajaram", marks=50):
    
      self.__name = name
      self.__marks = marks
    defstudentdata(self):print("Name: {} marks: {}".format(self.__name, self.__marks))
      
    s1 = Student() s2 = Student("Bharat",25) s1.studentdata() s2.studentdata()print("Name: {} marks: {}".format(s1.__name, s2.__marks))print("Name: {} marks: {}".format(s2.__name, __s2.marks))

    When you run this code, it will produce the following output −

    Name: Rajaram marks: 50
    Name: Bharat marks: 25
    Traceback (most recent call last):
     File "C:\Python311\hello.py", line 14, in <module>
      print ("Name: {} marks: {}".format(s1.__name, s2.__marks))
    AttributeError: 'Student' object has no attribute '__name'
    

    The above output makes it clear that the instance variables name and age, can be accessed by a method declared inside the class (the studentdata() method), but the double underscores prefix makes the variables private, and hence, accessing them outside the class is restricted which raises Attribute error.

    What is Name Mangling?

    Python doesn’t block access to private data entirely. It just leaves it to the wisdom of the programmer, not to write any code that accesses it from outside the class. You can still access the private members by Python’s name mangling technique.

    Name mangling is the process of changing name of a member with double underscore to the form object._class__variable. If so required, it can still be accessed from outside the class, but the practice should be refrained.

    In our example, the private instance variable “__name” is mangled by changing it to the format

    obj._class__privatevar
    

    So, to access the value of “__marks” instance variable of “s1” object, change it to “s1._Student__marks”.

    Change the print() statement in the above program to −

    print(s1._Student__marks)

    It now prints 50, the marks of s1.

    Hence, we can conclude that Python doesn’t implement encapsulation exactly as per the theory of object-oriented programming. It adapts a more mature approach towards it by prescribing a name convention and letting the programmer use name mangling if it is really required to have access to private data in the public scope.

  • Abstraction

    Abstraction is one of the important principles of object-oriented programming. It refers to a programming approach by which only the relevant data about an object is exposed, hiding all the other details. This approach helps in reducing the complexity and increasing the efficiency of application development.

    Types of Python Abstraction

    There are two types of abstraction. One is data abstraction, wherein the original data entity is hidden via a data structure that can internally work through the hidden data entities. Another type is called process abstraction. It refers to hiding the underlying implementation details of a process.

    Python Abstract Class

    In object-oriented programming terminology, a class is said to be an abstract class if it cannot be instantiated, that is you can have an object of an abstract class. You can however use it as a base or parent class for constructing other classes.

    Create an Abstract Class

    To create an abstract class in Python, it must inherit the ABC class that is defined in the ABC module. This module is available in Python’s standard library. Moreover, the class must have at least one abstract method. Again, an abstract method is the one which cannot be called but can be overridden. You need to decorate it with @abstractmethod decorator.

    Example: Create an Absctract Class

    from abc import ABC, abstractmethod
    classdemo(ABC):@abstractmethoddefmethod1(self):print("abstract method")returndefmethod2(self):print("concrete method")

    The demo class inherits ABC class. There is a method1() which is an abstract method. Note that the class may have other non-abstract (concrete) methods.

    If you try to declare an object of demo class, Python raises TypeError −

       obj = demo()
    
         ^^^^^^
    TypeError: Can't instantiate abstract class demo with abstract method method1

    The demo class here may be used as parent for another class. However, the child class must override the abstract method in parent class. If not, Python throws this error −

    TypeError: Can't instantiate abstract class concreteclass with abstract method method1
    

    Abstract Method Overriding

    Hence, the child class with the abstract method overridden is given in the following example −

    Example

    from abc import ABC, abstractmethod
    classdemoclass(ABC):@abstractmethoddefmethod1(self):print("abstract method")returndefmethod2(self):print("concrete method")classconcreteclass(democlass):defmethod1(self):super().method1()return
    
      
    obj = concreteclass() obj.method1() obj.method2()

    Output

    When you execute this code, it will produce the following output −

    abstract method
    concrete method