Advanced Python By Example Part — 2
By: Vishal Mishra
Tags: Data Science, Data Analyst, Object Oriented, Python, Class

Advanced Python By Example Part — 2

Welcome to the part-2 of the advanced python series. In the first chapter, we have understood important python concepts like Iterators, Generators, and Decorators in Python. In this chapter, we are going to learn all about Object-Oriented Programming.

Image By Author

Object-Oriented Programming in Python

Object-Oriented programming is very important as in every programming language, it is essential to modularize the code so that it can be re-used. We don’t want to write similar code at multiple places and also we want to provide a proper structure to the code so that it’s easier to understand. Hence to solve these and many other problems, we use Object-Oriented Programming. It’s not specific to any language however it’s a programming paradigm which many languages follow.

Classes and Instance Variables

Why we should use Classes. Well, Classes are used in most modern programming languages. They provide a way to logically group data and functions. Data correspond to attributes or properties and functions correspond to a method.

Let’s see this with an example. Suppose you are part of a superhero company (like Vought from the Boys tv series) and want to build an application for your company where you need to keep the track of all the superheroes. Hence to represent a Super Hero, you can use class for it.

Home

Black

You can define a class with the class keyword. In the above example, I have not provided any attributes or methods just used the pass keyword which means the class is not doing anything. however, I am creating 2 instances of the class — emp_1 and emp_2. Instance variables contain data that is unique to each instance. Here I am manually defining the instance variable so if I have to do this for all the employees in the organization, it’s very painful and we don’t exactly get the benefit of the class.

So now we have to improve our code. Let’s redefine the class and create the same 2 employees.

50000

Home.Lander@vought.com

Black.Noir@vought.com



Another way of getting hero_1 name

Home.Lander@vought.com

This looks better as you have created a class blueprint and you can create any number of class objects i.e. employees. Let’s understand a few points here.

__init__ is a special method of the class which is also known as the Constructor in other programming languages. We use these special methods to define an object or instantiate an object. This basically tells us how objects are different from each other. self means the instance itself. When we create an object, it passes the instance variable. You can provide any name for the instance variable but by convention everyone uses self. You can also call a method using the class name but in that case, you also have to provide the instance name (Employee.getemail(emp_1)). emp_1 is the instance variable that will be referred to as self inside the class.

Class Variables

Class variables are the variables that are shared amongst all instances of the class. Instance variables are unique to each instance however class variables will share the same value for all the instances.

Let’s understand this with a use case. Suppose you want to keep track of the number of employees in the organization. The number of employees can be a variable for which value will be the same across all the employees so we can define this as a class variable.

No of Employees before: 0

No of Employees after: 2

You can also use the self keyword with the class variables instead of the class name. There could be a scenario where you have defined a class variable with a particular value which will be the same for most of the instances but for a particular instance, you need to provide a different value. In that case, it’s better to use self.class_valiable_name instead of class.class_variable_name in the __init__ method and override the value with instance_name.class_variable_name.

So whenever you call a class variable with the instance name, first it checks for that variable in the instance variables, and if not found then check for the class level variables.

Regular Methods, class methods, and static methods

As you know by now that Regular Methods in the class automatically takes instance as a first argument. We call it self. Now how we can change this so that it can take Class itself as a first argument.

To create a class method, just add ‘@classmethod’ a line before creating the class method. The class is automatically the first argument to be passed in, and is represented as ‘cls’ instead of ‘class’. This is because ‘class’ is a python keyword reserved to create a class. So when do we use class methods? class methods can be used as alternative constructors.

Suppose there is a requirement where we have data in form of String separated by ‘-’ and we have to parse and split the string then pass those to create an Employee object. This can be done using class methods.

Queen.Maeve@vought.com

25000

Static methods are different from regular methods and class methods in that it doesn’t have a class or instance that is automatically passed in as a first positional argument. They can be created by adding ‘@staticmethod’ a line before defining the method. These are methods that have a logical connection to the Class but do not need a class or instance as an argument. However, you call a static method just like a class method — using a class name. We use static methods when there is no requirement for an instance or class variables inside the methods. So static methods are just the regular functions but we organize them inside the class.

LaserEyes

LaserEyes

LaserEyes

LaserEyes

LaserEyes

Inheritance

Inheritance allows us to create a new class that shares the same attributes and method as the original function, and add some extra functionality to the new class.

You can inherit a class simply by putting the inherited class in the parenthesis.

class SuperHeroes(Heroes):

When we call a method from a class, python follows the ‘method resolution order’, which is the chain of classes that it goes through to find what the method is. All classes have a built-in group of methods and attributes as their primary order.

Image by Author

In the above example, we can see that the SuperHeores class inherited all the attributes and methods from the super class — Heroes. In this example, sub class is not doing anything. It’s just inherited all the things from the super class. Let’s define the sub class properly. Suppose you want to extend the class and want to provide an additional attribute rank to the superheroes. To make this maintainable, you don’t have to instantiate older attributes again like the base class. Instead, you can use super() function to call the base class __init__ method.

A.Train@vought.com

1

You can call the base class __init__ method in 2 ways -

With instance ==> super().__ init __(attr1, attr2,..) With class itself ==> BaseClass.__ init __(self, attr1, attr2,…)

Below are some useful methods which come in handy while coding inheritance.

.isinstance(instance, class)

This method returns a boolean value of whether an instance belongs to a class or not.

2. .issubclass(subclass, class)

This method returns a boolean value of whether a class has inherited from another class.

Special Methods in Python

Python provides a variety of special methods that are very useful to perform some kind of operation. Special methods are represented by __<method_name>__ in python.

Suppose you want to print out an instance that you have created. It’s just going to print an object with its memory location.

<__main__.Heroes object at 0x7f9bb3e30ad0>

You see it’s printing the object with its memory location. But it would be nice to change the behavior so that it can print out something user friendly. That’s where special methods are going to come in handy. By defining special methods, we are going to change some of the built-in behavior and operations. Special methods are called implicitly when we create an object of the class. __init__ is also a special method which we have been using since starting. Let's understand 2 more common and frequently used special methods.

__repr__ __str__

__repr__ is used mostly for debugging purposes and __str__ is used for the display of an object to the end-user. You should specify at least __repr__ at the minimum level so that whenever you create an object, this method will be called. If you are manually calling __str__ method, then it checks for this method and if it's not found then it will eventually call __repr__ .

Heroes("Home", "Lander","50000")

Heroes("Home", "Lander",50000)

Home Lander - Home.Lander@vought.com

Home Lander - Home.Lander@vought.com

Operator Overloading in Python

Operator overloading allows the same operator to have different meanings according to the context. For Example => ‘+’ performs differently in the case of numbers, list, and string. In case of (2 + 3), it returns 5 and in case of (‘a’ + ‘b’), it returns ‘ab’.

Operator overloading can be implemented by overloading the special/magic method.

Below are a few examples. You can call magic methods/Special Methods in Python and perform custom handling.

+ __add__(self, other)

- __sub__(self, other)

* __mul__(self, other)

/ __truediv__(self, other)

// __floordiv__(self, other)

% __mod__(self, other)

** __pow__(self, other)



__lt__(self, other)

__gt__(self, other)

So here is an example => Let’s say you want to add the salaries of 2 Heroes just by calling + operator. Let’s start coding for it.

Total salary for Home Lander and Black Noir is: 120000

So in this way you can perform operator overloading and custom task.

Property Decorators — Getters, Setters, and Deleters

In theory, a Getter is a method that gets the value of a property. In object-oriented programming, this helps to access private attributes from a class.

Similarly, a setter is a method that sets the value of a property. With setters, you can set the value of private attributes in a class.

And last but not the least, Deleters are used to delete the value of a property. After setting this, the value of the property will be None.

Basically, using getters and setters ensures data encapsulation which is very essential in object-oriented programming as you don’t want to expose the internal implementation of the class and property.

Getters and Setters using normal methods

Before change:

50000

After change:

30000

There is a better way of using getters and setters using @property decorators.

Getters and Setters using property decorator

Before change:

50000

After change:

30000

Let’s see another example. In the below example we have an attribute email which is made up of 2 another attributes first and last . Suppose you change the value of first after creating the object but in this case, the value of the attribute email will not change and when you created the object, it called the __init__ constructor and set the value of email . So, this is not expected.

Before change:

Home.Lander@vought.com



After change:

Star

Home.Lander@vought.com

There could be one way where you remove email attributes from the __init__ method and create another method. But in this case, you will have to call a function email() instead of an attribute email . We got the expected result but this is not maintainable.

Before change:

Home.Lander@vought.com



After changing the first name:

Star

Star.Lander@vought.com

Let’s solve this using a property decorator. Where you don’t have to call as a function email() .

Before change:

Home.Lander@vought.com



After changing the first name using property decorator:

Star

Star.Lander@vought.com

Now suppose you want to change the value of email at run time and want to change the values of those attributes which form the email attribute. If you set without the setter method, it will throw an error.

Hence we will use a setter decorator to solve this problem.

Before change:

Home.Lander@vought.com



After changing the email:

Star

Star.Light@vought.com

Deleters

You can also delete attributes using deleter decorator.

Before change:

Home.Lander@vought.com



After deleting:

None

None.None@vought.com

Now, you don’t want to get an email like None.None@vought.com. So let’s improve our code — getter method.



Home.Lander@vought.com



After deleting:

None

Email is not set. Please set it using setters. Before change:After deleting:NoneEmail is not set. Please set it using setters.

Conclusion

We have come to the end of this chapter. I know we have learned a lot in this chapter and we might have to review it again to properly understand the concept. Please feel free to reach out to me in case of any improvements/suggestions.

Below is the link for the notebook.

You can find chapter-1 here.

I’ll come up with the next chapter (Chapter-3) soon.

Thanks a lot for reading.

Advance Python Series

Chapter — 1: Iterators, Generators, and Decorators in Python

Chapter — 2: Object-Oriented Programming in Python

Chapter — 3: Exception Handling and File Handling in Python