Table of Contents

  • What Is Object-Oriented Programming (OOP)?
  • Class and Object
  • Class or Static Variables and Instance Variables
  • Class methods, static methods and instance methods
  • Inheritance, Polymorphism, Abstraction, Encapsulation
  • Methods Overloading and Method Overriding
  • Access Modifiers - Public, Private and Protected
  • Getter, Setter and Property

 

What Is Object-Oriented Programming (OOP)?

Python is a multi-paradigm programming language. Meaning, it supports different programming approach.

One of the popular approach to solve a programming problem is by creating objects. This is known as Object-Oriented Programming (OOP).

An object has two characteristics:

  • attributes
  • behavior

Let's take an example:

Parrot is an object,

  • name, age, color are attributes
  • singing, dancing are behavior

The concept of OOP in Python focuses on creating reusable code. This concept is also known as DRY (Don't Repeat Yourself).

In Python, the concept of OOP follows some basic principles:

Inheritance                                                     A process of using details from a new class without modifying existing class.
EncapsulationHiding the private details of a class from other objects.
PolymorphismA concept of using common operation in different ways for different data input.

Class

A class is a blueprint for the object.

We can think of class as an sketch of a parrot with labels. It contains all the details about the name, colors, size etc. Based on these descriptions, we can study about the parrot. Here, parrot is an object.

The example for class of parrot can be :

class Parrot:
    pass

Here, we use class keyword to define an empty class Parrot. From class, we construct instances. An instance is a specific object created from a particular class.

Object

An object (instance) is an instantiation of a class. When class is defined, only the description for the object is defined. Therefore, no memory or storage is allocated.

The example for object of parrot class can be:

obj = Parrot()

Here, obj is object of class Parrot.

Class or Static Variable and Instance Variable

Class or static variables are shared by all objects. Instance or non-static variables are different for different objects (every object has a copy of it).

All variables which are assigned a value in class declaration and classmethod are class variables. And variables which are assigned values inside instance methods are instance variables.

Example

class CSStudent:
    stream = 'cse'                  # Class or Static Variable
    def __init__(self,name,roll):
        self.name = name            # Instance Variable
        self.roll = roll            # Instance Variable
    # Objects of CSStudent class
    a = CSStudent('Nitin', 1)
    b = CSStudent('Kanchan', 2)

    print(a.stream)  # prints "cse"
    print(b.stream)  # prints "cse"
    print(a.name)    # prints "Nitin"
    print(b.name)    # prints "Kanchan"
    print(a.roll)    # prints "1"
    print(b.roll)    # prints "2"

    # Class variables can be accessed using class
    # name also
    print(CSStudent.stream) # prints "cse"

Class, Static and Instance Method

Let’s begin by writing a (Python 3) class that contains simple examples for all three method types

class MyClass:
    def method(self):
        return 'instance method called', self

    @classmethod
    def classmethod(cls):
        return 'class method called', cls

    @staticmethod
    def staticmethod():
        return 'static method called'

 

Instance Methods

The first method on MyClass, called method, is a regular instance method. That’s the basic, no-frills method type you’ll use most of the time. You can see the method takes one parameter, self, which points to an instance of MyClass when the method is called (but of course instance methods can accept more than just one parameter).

Through the self parameter, instance methods can freely access attributes and other methods on the same object. This gives them a lot of power when it comes to modifying an object’s state.

Not only can they modify object state, instance methods can also access the class itself through the self.__class__ attribute. This means instance methods can also modify class state.

Class Methods

Let’s compare that to the second method, MyClass.classmethod. I marked this method with a @classmethod decorator to flag it as a class method.

Instead of accepting a self parameter, class methods take a cls parameter that points to the class—and not the object instance—when the method is called.

Because the class method only has access to this cls argument, it can’t modify object instance state. That would require access to self. However, class methods can still modify class state that applies across all instances of the class.

Static Methods

The third method, MyClass.staticmethod was marked with a @staticmethod decorator to flag it as a static method.

This type of method takes neither a self nor a cls parameter (but of course it’s free to accept an arbitrary number of other parameters).

Therefore a static method can neither modify object state nor class state. Static methods are restricted in what data they can access - and they’re primarily a way to namespace your methods.

 

    Let’s See Them In Action!

    Instance Methods

    >>> obj = MyClass()
    >>> obj.method()
    ('instance method called', <MyClass instance at 0x101a2f4c8>)

    This confirmed that method (the instance method) has access to the object instance (printed as <MyClass instance>) via the self argument.

    When the method is called, Python replaces the self argument with the instance object, obj. We could ignore the syntactic sugar of the dot-call syntax (obj.method()) and pass the instance object manually to get the same result:

    >>> MyClass.method(obj)
    ('instance method called', <MyClass instance at 0x101a2f4c8>)

    Can you guess what would happen if you tried to call the method without first creating an instance?

    By the way, instance methods can also access the class itself through the self.__class__ attribute. This makes instance methods powerful in terms of access restrictions - they can modify state on the object instance and on the class itself.

    Class Methods

    >>> obj.classmethod()
    ('class method called', <class MyClass at 0x101a2f4c8>)

    Calling classmethod() showed us it doesn’t have access to the <MyClass instance> object, but only to the <class MyClass> object, representing the class itself (everything in Python is an object, even classes themselves).

    Notice how Python automatically passes the class as the first argument to the function when we call MyClass.classmethod(). Calling a method in Python through the dot syntax triggers this behavior. The self parameter on instance methods works the same way.

    Please note that naming these parameters self and cls is just a convention. You could just as easily name them the_object and the_class and get the same result. All that matters is that they’re positioned first in the parameter list for the method.

    Static Methods

    >>> obj.staticmethod()
    'static method called'

    Did you see how we called staticmethod() on the object and were able to do so successfully? Some developers are surprised when they learn that it’s possible to call a static method on an object instance.

    Behind the scenes Python simply enforces the access restrictions by not passing in the self or the cls argument when a static method gets called using the dot syntax.

    This confirms that static methods can neither access the object instance state nor the class state. They work like regular functions but belong to the class’s (and every instance’s) namespace.

    Now, let’s take a look at what happens when we attempt to call these methods on the class itself - without creating an object instance beforehand

    >> MyClass.classmethod()
    ('class method called', <class MyClass at 0x101a2f4c8>)
    
    >> MyClass.staticmethod()
    'static method called'
    
    >> MyClass.method()
    TypeError: unbound method method() must
        be called with MyClass instance as first
        argument (got nothing instead)

    We were able to call classmethod() and staticmethod() just fine, but attempting to call the instance method method() failed with a TypeError.

    And this is to be expected — this time we didn’t create an object instance and tried calling an instance function directly on the class blueprint itself. This means there is no way for Python to populate the self argument and therefore the call fails.

    Class method vs Static Method

    • A class method takes cls as first parameter while a static method needs no specific parameters.
    • A class method can access or modify class state while a static method can’t access or modify it.
    • In general, static methods know nothing about class state. They are utility type methods that take some parameters and work upon those parameters. On the other hand class methods must have class as parameter.
    • We use @classmethod decorator in python to create a class method and we use @staticmethod decorator to create a static method in python.

    When to use what?

    • We generally use class method to create factory methods. Factory methods return class object ( similar to a constructor ) for different use cases.
    • We generally use static methods to create utility functions.

    Inheritance, Polymorphism, Encapsulation, Abstraction

    Inheritance

    Inheritance is a way of creating new class for using details of existing class without modifying it. The newly formed class is a derived class (or child class). Similarly, the existing class is a base class (or parent class).

    # parent class
    class Bird:
        
        def __init__(self):
            print("Bird is ready")
    
        def whoisThis(self):
            print("Bird")
    
        def swim(self):
            print("Swim faster")
    
    # child class
    class Penguin(Bird):
    
        def __init__(self):
            # call super() function
            super().__init__()
            print("Penguin is ready")
    
        def whoisThis(self):
            print("Penguin")
    
        def run(self):
            print("Run faster")
    
    peggy = Penguin()
    peggy.whoisThis()
    peggy.swim()
    peggy.run()

    When we run this program, the output will be:

    # Output
    
    Bird is ready
    Penguin is ready
    Penguin
    Swim faster
    Run faster

    In the above program, we created two classes i.e. Bird (parent class) and Penguin (child class). The child class inherits the functions of parent class. We can see this from swim()method. Again, the child class modified the behavior of parent class. We can see this from whoisThis() method. Furthermore, we extend the functions of parent class, by creating a new run() method.

    Additionally, we use super() function before __init__() method. This is because we want to pull the content of __init__() method from the parent class into the child class.

    Encapsulation

    Using OOP in Python, we can restrict access to methods and variables. This prevent data from direct modification which is called encapsulation. In Python, we denote private attribute using underscore as prefix i.e single “_ “ or double “ __“.

    class Computer:
    
        def __init__(self):
            self.__maxprice = 900
    
        def sell(self):
            print("Selling Price: {}".format(self.__maxprice))
    
        def setMaxPrice(self, price):
            self.__maxprice = price
    
    c = Computer()
    c.sell()
    
    # change the price
    c.__maxprice = 1000
    c.sell()
    
    # using setter function
    c.setMaxPrice(1000)
    c.sell()

    When we run this program, the output will be:

    # Output
    
    Selling Price: 900
    Selling Price: 900
    Selling Price: 1000

    In the above program, we defined a class Computer. We use __init__() method to store the maximum selling price of computer. We tried to modify the price. However, we can’t change it because Python treats the __maxprice as private attributes. To change the value, we used a setter function i.e setMaxPrice() which takes price as parameter.

    Polymorphism

    Polymorphism is an ability (in OOP) to use common interface for multiple form (data types).

    Suppose, we need to color a shape, there are multiple shape option (rectangle, square, circle). However we could use same method to color any shape. This concept is called Polymorphism.

    class Parrot:
    
        def fly(self):
            print("Parrot can fly")
        
        def swim(self):
            print("Parrot can't swim")
    
    class Penguin:
    
        def fly(self):
            print("Penguin can't fly")
        
        def swim(self):
            print("Penguin can swim")
    
    # common interface
    def flying_test(bird):
        bird.fly()
    
    #instantiate objects
    blu = Parrot()
    peggy = Penguin()
    
    # passing the object
    flying_test(blu)
    flying_test(peggy)

    When we run above program, the output will be:

    # Output
    
    Parrot can fly
    Penguin can't fly

    In the above program, we defined two classes Parrot and Penguin. Each of them have common method fly() method. However, their functions are different. To allow polymorphism, we created common interface i.e flying_test() function that can take any object. Then, we passed the objects blu and peggy in the flying_test() function, it ran effectively.

    Abstraction

    Abstract classes 

    Force a class to implement methods.

    Abstract classes can contain abstract methods: methods without an implementation.
    Objects cannot be created from an abstract class. A subclass can implement an abstract class.

    Abstract methods

    But why?

    If you have many objects of a similar type, you can call them in a similar fashion.

    Imagine having classes like Truck, Car and Bus. They would all have methods like Start, Stop, Accelerate. An abstract class (Automobile) can define these abstract methods.

    truck.start()
    truck.drive()
    bus.start()
    bus.drive()

    When a new class is added, a developer does not need to look for methods to implement. He/she can simply look at the abstract class.

    If one of the sub classes (Truck, Car, Bus) misses an implementation, Python automatically throws an error.

    Abstract class example

    Create an abstract class: AbstractAnimal. In the abstract class we only define the methods without an implementation.

    You can then create concrete classes: classes containing an implementation. Let’s create a class Duck which implements the AbstractAnimal. We use the same methods, but now add an implementation.

    import abc
    
    class AbstractAnimal(object):
        __metaclass__ = abc.ABCMeta
    
        @abc.abstractmethod
        def walk(self):
            ''' data '''
    
        @abc.abstractmethod
        def talk(self):
            ''' data '''
    
    class Duck(AbstractAnimal):
        name = ''
    
        def __init__(self, name):
            print('duck created.')
            self.name = name
    
        def walk(self):
            print('walks')
    
        def talk(self):
            print('quack')
    
    obj = Duck('duck1')
    obj.talk()
    obj.walk()
    # Output
    
    Traceback (most recent call last):
      File "abst.py", line 27, in <module>
        obj = Duck('duck1')
    TypeError: Can't instantiate abstract class Duck with abstract methods walk

    If we forget to implement one of the abstract methods, Python will throw an error.

    This is how you can force classes to have methods. Those methods need to be defined in the abstract class.

    Method Overloading & Method Overriding

    Method Overloading

    Method overloading refers to the property of a method to behave in different ways depending upon the number or types of the parameters. Take a look at a very simple example of method overloading. Execute the following script:

    # Creates class Car
    class Car:
       def start(self, a, b=None):
            if b is not None:
                print (a + b)
            else:
                print (a)
    

    In the script above, if the start() method is called by passing a single argument, the parameter will be printed on the screen. However, if we pass 2 arguments to the start() method, it will add both the arguments and will print the result of the sum.

    Let's try with single argument first:

    car_a = Car()
    car_a.start(10)
    

    In the output, you will see 10. Now let's try to pass 2 arguments:

    car_a.start(10,20)
    

    In the output, you will see 30.

    Method Overriding

    Method overriding refers to having a method with the same name in the child class as in the parent class. The definition of the method differs in parent and child classes but the name remains the same. Let's take a simple example method overriding in Python.

    # Create Class Vehicle
    class Vehicle:
        def print_details(self):
            print("This is parent Vehicle class method")
    
    # Create Class Car that inherits Vehicle
    class Car(Vehicle):
        def print_details(self):
            print("This is child Car class method")
    
    # Create Class Cycle that inherits Vehicle
    class Cycle(Vehicle):
        def print_details(self):
            print("This is child Cycle class method")
    
    # Class Cycle can also be imlemented like this for showing overriding
    class Cycle(Vehicle,Car):
        pass
    

    In the script above the Car and Cycle classes inherit the Vehicle class. The vehicle class has print_details() method, which is overridden by the child classes. Now if you call the print_details() method, the output will depend upon the object through which the method is being called. Execute the following script to see this concept in action:

    car_a = Vehicle()
    car_a. print_details()
    
    car_b = Car()
    car_b.print_details()
    
    car_c = Cycle()
    car_c.print_details()
    

    The output will look like this:

    This is parent Vehicle class method
    This is child Car class method
    This is child Cycle class method
    

    You can see that the output is different, although the print_details() method is being called through derived classes of the same base class. However, since the child classes have overridden the parent class method, the methods behave differently.

     

    Access Modifiers - Public, Private and Protected

    Classical object-oriented languages, such as C++ and Java, control the access to class resources by public, private and protected keywords. Private members of a class are denied access from the environment outside the class. They can be handled only from within the class.

    Public members (generally methods declared in a class) are accessible from outside the class. The object of the same class is required to invoke a public method. This arrangement of private instance variables and public methods ensures the principle of data encapsulation.

    Protected members of a class are accessible from within the class and are also available to its sub-classes. No other environment is permitted access to it. This enables specific resources of the parent class to be inherited by the child class.

    Python doesn't have any mechanism that effectively restricts access to any instance variable or method. Python prescribes a convention of prefixing the name of the variable/method with single or double underscore to emulate the behaviour of protected and private access specifiers.

    All members in a Python class are public by default. Any member can be accessed from outside the class environment.

    Example: Public Attributes

    class employee:
        def __init__(self, name, sal):
            self.name=name
            self.salary=sal
    

    You can access employee class's attributes and also modify their values, as shown below.

    >>> e1=Employee("Kiran",10000)
    >>> e1.salary
    10000
    >>> e1.salary=20000
    >>> e1.salary
    20000

    Python's convention to make an instance variable protected is to add a prefix _ (single underscore) to it. This effectively prevents it to be accessed, unless it is from within a sub-class.

    Example: Protected Attributes

    class employee:
        def __init__(self, name, sal):
            self._name=name  # protected attribute 
            self._salary=sal # protected attribute
    

    In fact, this doesn't prevent instance variables from accessing or modifyingthe instance. You can still perform the following operations:

    >>> e1=employee("Swati", 10000)
    >>> e1._salary
    10000
    >>> e1._salary=20000
    >>> e1._salary
    20000

    Hence, the responsible programmer would refrain from accessing and modifying instance variables prefixed with _ from outside its class.

    Similarly, a double underscore __ prefixed to a variable makes it private. It gives a strong suggestion not to touch it from outside the class. Any attempt to do so will result in an AttributeError:

    Example: Private Attributes

    class employee:
        def __init__(self, name, sal):
            self.__name=name  # private attribute 
            self.__salary=sal # private attribute
    
    >>> e1=employee("Bill",10000)
    >>> e1.__salary
    AttributeError: 'employee' object has no attribute '__salary'

    Python performs name mangling of private variables. Every member with double underscore will be changed to _object._class__variable. If so required, it can still be accessed from outside the class, but the practice should be refrained.

    >>> e1=Employee("Bill",10000)
    >>> e1._Employee__salary
    10000
    >>> e1._Employee__salary=20000
    >>> e1._Employee__salary
    20000

    Getter, Setter and Property

    An Example To Begin With

    Let us assume that you decide to make a class that could store the temperature in degree Celsius. It would also implement a method to convert the temperature into degree Fahrenheit. One way of doing this is as follows.

    class Celsius:
        def __init__(self, temperature = 0):
            self.temperature = temperature
    
        def to_fahrenheit(self):
            return (self.temperature * 1.8) + 32

    We could make objects out of this class and manipulate the attribute temperature as we wished. Try these on Python shell.

    >>> # create new object
    >>> man = Celsius()
    >>> # set temperature
    >>> man.temperature = 37
    >>> # get temperature
    >>> man.temperature
    37
    >>> # get degrees Fahrenheit
    >>> man.to_fahrenheit()
    98.60000000000001

    The extra decimal places when converting into Fahrenheit is due to the floating point arithmetic error (try 1.1 + 2.2 in the Python interpreter).

    Whenever we assign or retrieve any object attribute like temperature, as show above, Python searches it in the object's __dict__ dictionary.

    >>> man.__dict__
    {'temperature': 37}

    Therefore, man.temperature internally becomes man.__dict__['temperature'].

    Now, let's further assume that our class got popular among clients and they started using it in their programs. They did all kinds of assignments to the object.

    One fateful day, a trusted client came to us and suggested that temperatures cannot go below -273 degree Celsius (students of thermodynamics might argue that it's actually -273.15), also called the absolute zero. He further asked us to implement this value constraint. Being a company that strive for customer satisfaction, we happily heeded the suggestion and released version 1.01 (an upgrade of our existing class).

    Using Getters and Setters

    An obvious solution to the above constraint will be to hide the attribute temperature (make it private) and define new getter and setter interfaces to manipulate it. This can be done as follows.

    class Celsius:
        def __init__(self, temperature = 0):
            self.set_temperature(temperature)
    
        def to_fahrenheit(self):
            return (self.get_temperature() * 1.8) + 32
    
        # new update
        def get_temperature(self):
            return self._temperature
    
        def set_temperature(self, value):
            if value < -273:
                raise ValueError("Temperature below -273 is not possible")
            self._temperature = value

    We can see above that new methods get_temperature() and set_temperature() were defined and furthermore, temperature was replaced with  _temperature. An underscore (_) at the beginning is used to denote private variables in Python.

    >>> c = Celsius(-277)
    Traceback (most recent call last):
    ...
    ValueError: Temperature below -273 is not possible
    >>> c = Celsius(37)
    >>> c.get_temperature()
    37
    >>> c.set_temperature(10)
    >>> c.set_temperature(-300)
    Traceback (most recent call last):
    ...
    ValueError: Temperature below -273 is not possible

    This update successfully implemented the new restriction. We are no longer allowed to set temperature below -273.

    Please note that private variables don't exist in Python. There are simply norms to be followed. The language itself don't apply any restrictions.

    >>> c._temperature = -300
    >>> c.get_temperature()
    -300

    But this is not of great concern. The big problem with the above update is that, all the clients who implemented our previous class in their program have to modify their code from obj.temperature to obj.get_temperature() and all assignments like obj.temperature = val to obj.set_temperature(val).

    This refactoring can cause headaches to the clients with hundreds of thousands of lines of codes.

    All in all, our new update was not backward compatible. This is where property comes to rescue.

    The Power of @property

    The pythonic way to deal with the above problem is to use property. Here is how we could have achieved it.

    class Celsius:
        def __init__(self, temperature = 0):
            self.temperature = temperature
    
        def to_fahrenheit(self):
            return (self.temperature * 1.8) + 32
    
        def get_temperature(self):
            print("Getting value")
            return self._temperature
    
        def set_temperature(self, value):
            if value < -273:
                raise ValueError("Temperature below -273 is not possible")
            print("Setting value")
            self._temperature = value
    
        temperature = property(get_temperature,set_temperature)

    And, issue the following code in shell once you run it.

    >>> c = Celsius()

    We added a print() function inside get_temperature() and set_temperature() to clearly observe that they are being executed.

    The last line of the code, makes a property object temperature. Simply put, property attaches some code (get_temperature and set_temperature) to the member attribute accesses (temperature).

    Any code that retrieves the value of temperature will automatically call get_temperature()instead of a dictionary (__dict__) look-up. Similarly, any code that assigns a value to temperature will automatically call set_temperature(). This is one cool feature in Python.

    We can see above that set_temperature() was called even when we created an object.

    Can you guess why?

    The reason is that when an object is created __init__() method gets called. This method has the line self.temperature = temperature. This assignment automatically called set_temperature().

    >>> c.temperature
    Getting value
    0

    Similarly, any access like c.temperature automatically calls get_temperature(). This is what property does. Here are a few more examples.

    >>> c.temperature = 37
    Setting value
    
    >>> c.to_fahrenheit()
    Getting value
    98.60000000000001

    By using property, we can see that, we modified our class and implemented the value constraint without any change required to the client code. Thus our implementation was backward compatible and everybody is happy.

    Finally note that, the actual temperature value is stored in the private variable _temperature. The attribute temperature is a property object which provides interface to this private variable.


    Digging Deeper into Property

    In Python, property() is a built-in function that creates and returns a property object. The signature of this function is

    property(fget=None, fset=None, fdel=None, doc=None)

    where, fget is function to get value of the attribute, fset is function to set value of the attribute, fdel is function to delete the attribute and doc is a string (like a comment). As seen from the implementation, these function arguments are optional. So, a property object can simply be created as follows.

    >>> property()
    <property object at 0x0000000003239B38>

    A property object has three methods, getter()setter(), and deleter() to specify fgetfset and fdel at a later point. This means, the line

    temperature = property(get_temperature,set_temperature)

    could have been broken down as

    # make empty property
    temperature = property()
    # assign fget
    temperature = temperature.getter(get_temperature)
    # assign fset
    temperature = temperature.setter(set_temperature)

    These two pieces of codes are equivalent.

    Programmers familiar with decorators in Python can recognize that the above construct can be implemented as decorators.

    We can further go on and not define names get_temperature and set_temperature as they are unnecessary and pollute the class namespace. For this, we reuse the name temperature while defining our getter and setter functions. This is how it can be done.

    class Celsius:
        def __init__(self, temperature = 0):
            self._temperature = temperature
    
        def to_fahrenheit(self):
            return (self.temperature * 1.8) + 32
    
        @property
        def temperature(self):
            print("Getting value")
            return self._temperature
    
        @temperature.setter
        def temperature(self, value):
            if value < -273:
                raise ValueError("Temperature below -273 is not possible")
            print("Setting value")
            self._temperature = value

    The above implementation is both, simple and recommended way to make properties. You will most likely encounter these types of constructs when looking for property in Python.

    Well that's it for today.