# Quick Guide to Object-Oriented Programming With Python

# What is OOP (Object-Oriented Programming)?

If you try to look on the internet or in any textbooks, there is no specific definition of OOP. In general, object-oriented programming is considered a combination of 4 pillar concepts. Those concepts are:

1. Abstraction (To hide something)
    
2. Encapsulation (To encapsulate or join many things)
    
3. Inheritance (To inherit something)
    
4. Polymorphism (To use for many purposes)
    

I will explain these concepts in detail as the blog progresses. But the main thing to know in OOP is **Classes & Objects**. So, what are these Classes and Objects?

# Class & Object in Python

In simple terms, A class is just a skeleton or structure of an actual thing (object). For example, a sketch/outline of a building is a class, whereas, an actual building is an Object.

Syntax to create a class:

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

Now, if we apply this Class-Object concept to all humans, then we can have **Person** as a class with many properties and functionality.

```python
class Person:
	age = 30
	height = 180
	weight = 65

	def say_hello(self):
		print('Hello Person')
```

Here, we have created a Person class with **age, height** and **weight** properties and the **say\_hello()** method or functionality. We have created this Person class, now what?

To use this person class we need to create an object from this Person class. To create an object we use the following syntax:

```python
object_name = ClassName()
```

To create an object of Person class:

```python
person = Person()
print(person)

""" OUTPUT:

<__main__.Person object at 0x000001C47ED317B0>

"""
```

When we try to print `person`, we can see that it is an object of the Person class located at **0x000001C47ED317B0** in memory. To use the property and methods of the class, we can use the following syntax:

```python
class Person:
	age = 30
	height = 180
	weight = 65

	def say_hello(self):
		print('Hello Person')

person = Person()
print("AGE:", person.age)
print("HEIGHT:", person.height)
print("WEIGHt:", person.weight)

person.say_hello()

""" OUTPUT:

AGE: 30
HEIGHT: 180
WEIGHt: 65
Hello Person

"""
```

This Person class looks very static and with fixed functionality then why I was calling it a schema/outline you might ask?

I kept the class Person simple on purpose to show you the basic use of Class and Objects. So if I were to show you the proper yet simple use of class and object then:

```python
class Person:

	def __init__(self, name, age, height, weight):
		self.name  = name
		self.age = age
		self.height = height
		self.weight = weight

	def show_data(self):
		print('Details: ')
		print('AGE:', self.age)
		print('HEIGHT:', self.height, "cm")
		print('WEIGHt:', self.weight, "kg")

person1 = Person('John Doe', 33, 180, 75)
person1.show_data()

print("-"*15)

person2 = Person('Jane Doe', 31, 160, 65)
person2.show_data()

""" OUTPUT:
Details:
NAME: John Doe
AGE: 33
HEIGHT: 180 cm
WEIGHt: 75 kg
---------------
Details:
NAME: Jane Doe
AGE: 31
HEIGHT: 160 cm
WEIGHt: 65 kg
"""
```

As you can see in the above code example, I was able to pass different values for name, age, height and weight and created 2 objects from the Person class. I can pass the same or different values as per my need and create an N number of objects because they will be stored at different locations anyway.

```python
person1 = Person('John Doe', 33, 180, 75)
print(person1)
# OUTPUT: <__main__.Person object at 0x000001E6B1C93FA0>

person2 = Person('Jane Doe', 31, 160, 65)
print(person2)
# OUTPUT: <__main__.Person object at 0x000001E6B1C93EB0>

person3 = Person('Jane Doe', 31, 160, 65)
print(person3)
# OUTPUT: <__main__.Person object at 0x000001E6B1C93E50>
```

In the real-life scenario, when we have to create multiple users (signup functionality), we do not write code for every user. We use class and create objects based on the received details as we did with `person1` and `person2`.

Now, you might have noticed the `self` keyword and `__init__()` method in the above code. So, what are these?

## What is the **init**() method in Python?

If you know other languages such as Java, C# or JavaScript, you might be familiar with the concept of a constructor. The **init()** method is a constructor for a class in Python. For non-technical people, consider the constructor a way to tell the compiler/interpreter to initiate an operation or allocate memory to the object that we are planning to create. But it is not necessary to use the constructor in Python. If we do not provide the `__init__()` method, the interpreter will create on its own.

In general, we use the **init()** method to initialize some properties and for inheritance purposes. As we saw earlier, this is how we initialize the variables:

```python
def __init__(self, name, age, height, weight):
		self.name  = name
		self.age = age
		self.height = height
		self.weight = weight
```

***Note:*** We will discuss how we use the `__init__()` method for inheritance later.

Now, you might ask, but Sahil, what the heck `self` is?

## What is the ‘self’ keyword in Python?

The **self** keyword represents the instance of a class. (Just for simple understanding, consider instance as an object generated by the interpreter. **Objects and instances are not the same but here consider them the same, just for simple understanding.**)

Using this self keyword, we can access all attributes/properties and methods across the class. For instance, in the example below, we have used the `self` keyword to access **name**, **age**, **height** and **weight** attributes.

```python
class Person:

	def __init__(self, name, age, height, weight):
		self.name  = name
		self.age = age
		self.height = height
		self.weight = weight

	def show_data(self):
		print('Details: ')
		print('NAME:', self.name)
		print('AGE:', self.age)
		print('HEIGHT:', self.height, "cm")
		print('WEIGHt:', self.weight, "kg")
```

We can also access methods using the `self` keyword.

```python
class Person:

	def __init__(self, name, age, height, weight):
		self.name  = name
		self.age = age
		self.height = height
		self.weight = weight

	def show_data(self):
		print('Details: ')
		print('NAME:', self.name)
		print('AGE:', self.age)
		print('HEIGHT:', self.height, "cm")
		print('WEIGHt:', self.weight, "kg")

	def self_demo(self):
		self.show_data()

person1 = Person('John Doe', 33, 180, 75)
person1.self_demo()

""" OUTPUT:
Details:
NAME: John Doe
AGE: 33
HEIGHT: 180 cm
WEIGHt: 75 kg
"""
```

It is not necessary to use the `self` keyword. You can use any name, but the condition is, that it should be the first argument of a method. (For **init**() as well.)

```python
class Person:

	def __init__(random_replacement, name):
		random_replacement.name = name
		pass

	def show_data(random_replacement_1):
		print('Just a Random Thing!')
		print('Name:', random_replacement_1.name)

person1 = Person('John Doe')
person1.show_data()

""" OUTPUT:
Just a Random Thing!
Name: John Doe
"""
```

As you might have guessed, we don't need to use the same name across the class. The one and only rule is,`it should be the first argument!` But generally, we prefer the `self` keyword as it has become almost a standard. If we want to pass any arguments, we must pass them after the `self` keyword. As we did in the `__init__()` method.

You can also add any attribute at any time using the `self` keyword.

```python
class Person:

	def __init__(self, name):
		self.name = name

	def add_age(self, age):
		self.age = age

	def show_data(self):
		print('Name:', self.name)
		print('Age:', self.age)

person1 = Person('John Doe')
person1.add_age(31)
person1.show_data()

""" OUTPUT:
Name: John Doe
Age: 31
"""
```

Here, we have added **age** to the class attributes. I do not suggest you do that but just for demo purposes that it is possible. Consider the `self` keyword as the dictionary and all the attributes and methods as the `key` of that dictionary.

In the above code example, if we call the `show_data()` method first. It will throw an error, as we do not have any attribute named age at that time.

> This process of combining attributes and methods under one umbrella is called **encapsulation**.

So, how do we extend the usage and functionality of our class? We can do that using `Inheritance` and `Polymorphism`.

# Inheritance in Python

As the name suggests, inheritance is the capability to inherit something. In this case, we will be inheriting attributes and methods. For example, we created a class named Person which is too generic. By inheriting the generic class, we can create another class that will be more specific to our needs.

We want to create an **Employee** class that inherits everything or is inherited from the **Person** class. So, how do we do it?

**Inheritance syntax in Python:**

```python
# Base Class/Parent Class
class BaseClass:
		<statement-1>
    .
    <statement-N>

# Derived Class/Child Class
class DerivedClass(BaseClass):
		<statement-1>
    .
    <statement-N>
```

To create the **Employee** class from the **Person** class we can write the following code:

```python
class Person:
	def __init__(self, name, age, height, weight):
		self.name  = name
		self.age = age
		self.height = height
		self.weight = weight

	def show_basic_data(self):
		print('Details: ')
		print('NAME:', self.name)
		print('AGE:', self.age)
		print('HEIGHT:', self.height, "cm")
		print('WEIGHt:', self.weight, "kg")

class Employee(Person):
	def __init__(self, employeeId, department, salary):
		super().__init__(name, age, height, weight)
		self.employeeId = employeeId
		self.department = department
		self.salary = salary

	def show_data(self):
		self.show_basic_data()
		print('Employee Id:', self.employeeId)
		print('Department:', self.department)
		print('salary: $', self.salary)

employee = Employee('John Doe', 33, 180, 75, 1 ,'IT', 100000)
employee.show_data()

""" OUTPUT:
Details:
NAME: John Doe
AGE: 33
HEIGHT: 180 cm
WEIGHt: 75 kg
Employee Id: 1
Department: IT
salary: $ 100000
"""
```

You might have noticed `super()` in Employee class. What is `super()` you might ask? The **super** keyword refers to the Parent class. We use the `super()` method to access attributes and methods of the parent class. And that is what we did in the `__init__()` method of the Employee class. We passed some data to the parent class.

Once we have called **super()** in the `__init__()` method of the child class, we can use all the attributes and methods of the parent class in our child class. That is what we did in the **show\_data()** method of the Employee class. We could use `super().show_basic_data()` as well.

---

# **Conclusion**

Finally! We are at the end of this section 😁.

Let me know if you need any help or want to discuss something. Reach out to me on [Twitter](https://twitter.com/Sahil_Fruitwala) or [LinkedIn](https://bit.ly/3JbsPDm). Make sure to leave any thoughts, questions, or concerns in the comments below. I would love to see them.

> *Want to learn more? Signup for my Newsletter and get the best articles into your inbox.*

Till the next time 👋
