Tutorial: Object Oriented Programming — Language Python
Tags: python, oop, tutorial, learning
Published: 2023-07-14 19:44:49

## What is Object Oriented Programming?

- **Programming paradigm that organizes code around objects:**
Where we represent real-world entities like cars, buildings, animals and other various options within lines of code so that they are classified by object definitions. Using concepts to improve organization and maintenance.

- **Objects that are entities that have state (data):** Like real world objects that have characteristics within object orientation we can represent using attributes.

- **It also has behavior (methods):** In object orientation, it does not leave out the functionalities of entities (object) in the real world. We can represent through definitions of functions that represent the behavior of objects.


![Created by the author](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z6tlohvrzem1l4bqo4lo.png)

## What are the advantages and disadvantages of OOP?

### Benefits:

**Code reuse**

There will be no need to create each class for different objects, which have the same attributes. Like the car class, I won't need to create a class (lines of code) for each car model. As an example let's think that there is a factory called "OO Car", this factory produces two types of car "OO Turbo" and "OO 4x4". You agree with me that these cars have a factory, 4 wheels, 1 engine, color, ..... and several other parts. We won't go into all the details. But imagine you as a programmer, defining a class for each car and most of the time they will have the same details. Being in this context the company "OO Car", develops a new car. Again you will have to create a class with the same characteristics as the previous cars. So you will be creating a huge duplicity without having to. This is where object orientation comes in. We define only one class so that we can reuse it for different cars.

**Wrong**

```python


class Car_OO_Turbo():

     def __init__(self, model, engine, color):
         self.model = model
         self.engine = engine
         self.color = color

class Car_OO_4x4():
    
     def __init__(self, model, engine, color):
         self.model = model
         self.engine = engine
         self.color = color

```

**Correct**

```python


class Car():

     def __init__(self, model, engine, color):
         self.model = model
         self.engine = engine
         self.color = color

```

**Ease of maintenance**

Object orientation makes it easy to make changes within the code, making maintenance within classes isolated from external code, avoiding problems. In addition to bringing focus to the problem where it is occurring.

### Disadvantages:

**Complexity**

But not everything is an advantage as the code develops, we can come across huge class declarations, increasingly complex with attributes and methods. Relationships between classes, thus several related classes, which may affect maintenance.

**Debugging Difficulty**

More and more classes with inheritance, polymorphism, association and other patterns. They are more related to each other. This makes it difficult to look for problems and errors that the code presents. Taking more time to debug code.

## Concepts

-----

### Object

The object is defined as an entity so that we can represent something from the real world. For example:

- Car
- Address
- Home
- Food
- Plane

Only that object was not defined because of a label being a car or a house. Object has characteristics (Attributes) and processes (methods). So when we intend to represent an object within the OOP, we add these specifications so that its representation is comprehensive within the system to be developed.


![Created by the author](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x2glc170yb3ymdk7qra2.png)

-----

### Class

Class is intended to be a template, or a mold of how we want to design a real-world object within our project. With the class we avoid creating several objects, using a class to represent different objects and their characteristics are different, in the following steps this form of representation will become clearer.

Examples
- Car Class
- Customer class
- Book Class

Having only one class, we can define its attributes and methods.

Using the Python language we can create a class like the example below.

```python
class Car:
    ...
class Client:
    ...
class Book:
    ...
```

-----

### Attributes

Attributes are the characteristics of the object, using a restaurant as an example, the ordered object can be represented as a class. After its definition, we add the characteristics (Attributes) of the request to the class. Like the customer's name, chosen snack, point of meat, etc. You can observe in large fast-foods examples of order objects, when you choose, pay and wait to take the order you receive the receipt which in this case is your order with your name and another copy goes to the kitchen to be prepared with the characteristics of the your request. In some you don't even need receipt paper anymore. There is a monitor with your order inside the kitchen and another one outside with the order in which the orders are prepared.

- **"\_\_init__()"**: This method has a constructor definition within the Python language class, in this method where we will define class attributes.

- **"self"**: This parameter has its definition as a convention to be used by the internal methods of the class that we will see in the next step. Where we won't have the need to pass a parameter of the class to each method of the class. Where once the class is instantiated and **self** is defined inside the inner function. The use of attributes can be used.

```python
class Order:
    
     def __init__(
         self,
         customer_name: str,
         snack: str,
         meat_stitch: str,
         soda: bool,
         potato: bool,
     ):
         self.customer_name = customer_name
         self.snack = snack
         self.meat_point = meat_stitch
         self.soda = soda
         self.potato = potato

```
```python
order1 = Order('Severius', 'X-Bacon','To the point', False, False)

```
```python
print(
     f'Customer name: {order1.customer_name}\n'
     f'Snack Chosen: {order1.snack}\n'
     f'Meat point: {order1.meat_point}\n'
     f'Ordered soda: {order1.soda}\n'
     f'Ordered Potato: {order1.potato}\n'
)

# OUTPUT:
# Customer name: Severius
# Snack Chosen: X-Bacon
# Meat point: To the point
# Ordered soda: False
# Ordered Potato: False
```
```python
order2 = Order('Kratos', 'X-Fish','To the point', True, True)
```
```python
print(
     f'Customer name: {order2.customer_name}\n'
     f'Snack Chosen: {order2.snack}\n'
     f'Meat point: {order2.meat_point}\n'
     f'Ordered soda: {order2.soda}\n'
     f'Ordered Potato: {order2.potato}\n'
)

# output:
# Customer name: Kratos
# Snack Chosen: X-Fish
# Meat point: To the point
# Ordered soda: True
# Ordered Potato: True

```
-----

### Methods

Methods are functions that we represent of objects within classes. Using the car as an example, we can define functions such as turning on the engine, turning off the engines, turning on the headlights, turning off the headlights, activating the alarm, deactivating the alarm, moving, stopping, opening doors ........ and thousands of other functions that the car can have. If you are familiar with Python. We define a method within the class by writing an internal function of the class, which will remain only within its scope. Nothing outside the class will have access, unless we instantiate an object.

```python
class Car:
    
    def __init__(self, model):
        self.model = model
    
    def turn_on_car(self):
        return 'Car On'
    
    def turn_off_car(self):
        return 'Car off'
```
```python
car.turn_on_car()
#output: 'Car On'
```
```python
car.turn_off_car()
#output: 'Car off'
```

-----

### Attribute visibility

Attribute visibility brings an option within the Python language for sharing class functionality. That can happen externally or internally. Not always when we are going to develop software will we need to use external attributes or methods, so that other parts of the code do not have external access to the class and only internally protecting attributes and methods. In Python language there are 3 public, protected, private. But they have their language peculiarities where there are some differences from other programming languages.

**Public**

One of the access modifiers that the Python language has. It's **public** , it allows access to attributes and methods anywhere in the code without restrictions.

```python
class House:
    def __init__(self, color):
        self.color = color
```
```python
house_public = House('Blue public')
print(house_public.color)
# output: Blue public
```

**Protected**

The **protected** access modifier does not have the same function as in the Java language, it exists by convention or standard in the Python language. It is represented as **" _ "**. To declare in the Python language we add it as a prefix to the attribute or method. But it doesn't mean it's protected, it works as a convention among developers. In a way that indicates the access of attributes and methods, only in subclasses.

```python
class House:
    def __init__(self, color):
        self._color = color
        
    def print_color(self):
        print(f'Color this house is {self._color}')
```
```python
house_protected = House('Blue protected')
print(house_protected._color)
print(house_protected.print_color())

# output : Blue protected
# Color this house is Blue protected
# None
```

**Private**

Now we have **private** it also works as a Python language convention. It is represented as **" __ "**. To declare in the Python language we add it as a prefix to the attribute or method. But it doesn't mean it's private, it works as a convention between developers. In a way that indicates the access of attributes and methods, only in the class where it was defined.

```python
class House:
    def __init__(self, color):
        self.__color = color
        
    def print_color(self):
        print(f'Color this house is {self.__color}')
```

But because an error occurred when **private** is also a convention. This is because when we declare the attribute with **" __ "**, the language understands that we are using a private attribute and performs a transformation on its name. Called "name mangling", it adds a **" _ "** prefix, making it accessible only within the class, but not private as in the Java language.

```python
try:
    house_private = House('Blue private')
    print(house_private.__color)
    print(house_private.print_color())
except Exception as e:
    print(e)
#output: 'House' object has no attribute '__color'
```

To access the modified name, you would have to pass in the same way that name mangling is returning.

```python
class House:
    def __init__(self, color):
        self.__color = color
        
    def print_color(self):
        print(f'Color this house is {self.__color}')
```
```python
try:
    house_private = House('Blue private')
    print(house_private._House__color)
    print(house_private.print_color())
except Exception as e:
    print(e)

# output:
# Blue private
# Color this house is Blue private
# None
```

-----

### Encapsulation

As one of the main foundations of OOP, encapsulation aims to ensure the integrity of the class's internal attributes and methods. In addition to improving code maintenance because necessary changes are only internal to the class, bringing a controlled environment that changes do not affect the external code.

**wrong**

At this stage it doesn't mean that it is wrong, but as we are declaring a private attribute. We don't want to use "name mangling", and be able to use encapsulation.

```python
class Car:
    def __init__(self, model, motor):
        self.model = model
        self.__motor = motor
car = Car('Ferrari', 'V8')
```

```python
try:
    print(
        car.model,
        car.motor,
    )
except Exception as e:
    print(e)
# output: 'Car' object has no attribute 'motor'
```

**correct**

The getters and setters are controlled and also safe methods so that we can access (get) and also modify (set) the data that will be assigned in a class.

- Getter: Returns the assigned value. Without having to deal directly with the class attribute. Creating a layer of protection. In this example I will be using a decorator called **@property**. This decorator allows the defined method to have access to the attribute.

- Setter: Changes the assigned value. Its principle is to define a new value for the attribute. In this example I will be using a decorator called **@setter**. This decorator allows the defined method to modify the attribute.

```python
class Car:
    def __init__(self, model, motor):
        self.model = model
        self.__motor = motor
    
    #Getter
    @property
    def motor(self):
        return self.__motor
    
    #Setter
    @motor.setter
    def motor(self, motor):
        self.__motor = motor

car = Car('Ferrari', 'V8')  
```
```python
print(
        car.model,
        car.motor,
    )
# output: Ferrari V8
```

-----

### Method Overloading

Method overloading will bring the possibility within the class to create several functions (methods) for the same class. Not having the need to create a new class for different functionalities. But in Python language, it doesn't support overloading like in other languages. Below I create a class as an example. But notice that the error is caught showing that an argument is missing. Because the next method overwrites the previous one.

```python
class UnifyStrings:
    def unify(self, first_text, second_text):
        return first_text + second_text
    
    def unify(self, first_test, second_text, separator):
        group_text_input = [first_test, second_text]
        return separator.join(group_text_input)
```
```python
try:
    all_join = UnifyStrings()
    print(all_join.unify("Hey", "Python")) 
    print(all_join.unify("Hey", "Python", ", ")) 
except Exception as e:
    print(e)
# output: UnifyStrings.unify() missing 1 required positional argument: 'separator'
```
-----

### Association

Association is a concept that explains a relationship between objects. As for the relationship in which two objects or rather classes can share information with each other.

Notice in the code that I'm defining two classes each with their characteristics

```python
class Kitchen:
     def __init__(self, boss_kitchen, time):
         self.boss_kitchen = boss_kitchen
         self.time = time
         self._order = None
    
     @property
     def order(self):
         return self._order
    
     @order.setter
     def order(self, order):
         self._order = order


class Request:
     def __init__(
         self,
         order_number,
         customer_name,
         hamburguer,
         observation,
         ):
         self.order_number = order_number
         self.customer_name = customer_name
         self.hamburguer = hamburguer
         self.observation = observation
        
     def deliver_kitchen_order(self):
         return f'Order number: {self.order_number}\n'\
             f'Customer Name: {self.customer_name}\n'\
             f'Hamburg: {self.hamburguer}\n'\
             f'Note: {self.observation}\n'
```

I instantiate the Kitchen class, getting a head chef and his hours.

```python
kitchen = Kitchen('Frinaldio', 'nocturnal')
print(kitchen.boss_kitchen, kitchen.time)
# output: Frinaldio nocturnal
```
At this point, a customer arrives and places an order. In the same way as the previous class, I create a new object that has your order definitions.

```python
request1 = Request(
     order_number=1671,
     customer_name='Rorismaldo Cruz',
     hamburguer='X-All',
     observation='No onion',
)

print(
     request1.order_number,
     request1.customer_name,
     request1.hamburguer,
     request1.observation,
)
# output: 1671 Rorismaldo Cruz X-All No onion
```

But the order has to go somewhere, where the kitchen receives the order. Notice in the Kitchen class where I only have setter and getter methods. I will assign an Order that is a class and I will be associating the order to the kitchen where it will be receiving the method of the Order class that is "degar_order_cozinha()". Then notice that I'm using this associated method on my kitchen object. Where am I having access to the order attributes.


```python
kitchen.order = request1
print(kitchen.order.deliver_kitchen_order())

# output: Order number: 1671
#Customer Name: Rorismaldo Cruz
#Hamburg: X-All
#Note: No onion

```
```python
kitchen = Kitchen('Crilzancar', 'Morning')
print(kitchen.boss_kitchen, kitchen.time)

# output: Crilzancar Morning

```

```python
request123 = Request(
     order_number=9012,
     customer_name='Jorolmir Cunha',
     hamburguer='X-Egg',
     observation='No egg',
)

print(
     request123.order_number,
     request123.customer_name,
     request123.hamburguer,
     request123.observation,
)
# output: 9012 Jorolmir Cunha X-Egg No egg
```

```python
kitchen.order = request123
print(kitchen.order.deliver_kitchen_order())

# output: Order number: 9012
# Customer Name: Jorolmir Cunha
# Hamburg: X-Egg
# Note: No egg
```
-----

### Aggregation

In aggregation a class has other classes within its structure.

```python
# This step below I just created to simulate an order time and then the time it will take to prepare

import time
import datetime

named_tuple = time.localtime()
year = named_tuple.tm_year
month = named_tuple.tm_mon
day = named_tuple.tm_mday
hour = named_tuple.tm_hour
minute = named_tuple.tm_min
second = named_tuple.tm_sec

start_request_time = datetime.datetime(year, month, day, hour, minute, second)
final_order_time = start_request_time + datetime.timedelta(minutes = 40)
print(start_request_time)
print(final_order_time)

# output: 2023-05-21 15:10:29
# 2023-05-21 15:50:29
```
```python
class Kitchen:
     def __init__(self, head_kitchen):
         self.head_kitchen = head_kitchen
         self._orders = []
    
     def new_order(self, order):
         self._orders.append(order)
        
     def kitchen_orders(self):
         for order in self._orders:
             print(
                 f'Order number: {order.order_number}\n'\
                 f'Customer Name: {order.customer_name}\n'\
                 f'Head of Kitchen: {self.head_kitchen}\n'\
                 f'Hamburg: {order.hamburguer}\n'\
                 f'Note: {order.observation}\n'
                 f'Request Time: {start_request_time}\n'
                 f'Time ready: {final_order_time}\n'
             )
    
    
class Request:
     def __init__(
         self,
         order_number,
         customer_name,
         hamburguer,
         observation,
         ):
         self.order_number = order_number
         self.customer_name = customer_name
         self.hamburguer = hamburguer
         self.observation = observation
```

Again in a "hahaha" kitchen situation. I'm going to define a new kitchen object.

```python
kitchen = Kitchen('Terequelzio')

```
But imagine that this kitchen receives several requests, but instead of associating. I will be uniting several objects within a class, where I can use their methods and attributes within a single class. In the case of the kitchen object, you will have access to the orders that arrive and whoever is inside the kitchen will see the characteristics of the order and start the preparation.

```python
request1 = Request(
     order_number=1671,
     customer_name='Rorismaldo Cruz',
     hamburguer='X-All',
     observation='No onion',
)

request2 = Request(
     order_number=9012,
     customer_name='Jorolmir Cunha',
     hamburguer='X-Egg',
     observation='No egg',
)

kitchen.new_order(request1)
kitchen.new_order(request2)

kitchen.kitchen_orders()

# output:
# Order number: 1671
# Customer Name: Rorismaldo Cruz
# Head of Kitchen: Terequelzio
# Hamburg: X-All
# Note: No onion
# Request Time: 2023-05-21 15:10:29
# Time ready: 2023-05-21 15:50:29

# Order number: 9012
# Customer Name: Jorolmir Cunha
# Head of Kitchen: Terequelzio
# Hamburg: X-Egg
# Note: No egg
# Request Time: 2023-05-21 15:10:29
# Time ready: 2023-05-21 15:50:29
```
-----

### Composition

Composition is about classes being combined with other classes without the need for a parent class (main class). That way we can have a class that looks like a building block. We create it as a base and with other classes we can compose piece by piece the structure we want as a result.

```python
# This step below I just created to simulate an order time and then the time it will take to prepare

import time
import datetime

named_tuple = time.localtime()
year = named_tuple.tm_year
month = named_tuple.tm_mon
day = named_tuple.tm_mday
hour = named_tuple.tm_hour
minute = named_tuple.tm_min
second = named_tuple.tm_sec

start_request_time = datetime.datetime(year, month, day, hour, minute, second)
final_order_time = start_request_time + datetime.timedelta(minutes = 40)
print(start_request_time)
print(final_order_time)

# output:
# 2023-05-21 15:10:29
# 2023-05-21 15:50:29
 
```

```python
class Potato:
     def __init__(self, size, quantity):
         self.size = size
         self.quantity = quantity
    
class Hamburguer:
     def __init__(self, size, quantity):
         self.size = size
         self.quantity = quantity
    
class Soda:
     def __init__(self, size, quantity):
         self.size = size
         self.quantity = quantity


class Request:
     def __init__(self, customer_name):
         self.customer_name = customer_name
         self._potato = None
         self._hamburguer = None
         self._soda = None
    
     @property
     def potato(self):
         return self._potato
    
     @potato.setter
     def potato(self, potato):
         self._potato = potato
        
     @property
     def hamburguer(self):
         return self._hamburguer
    
     @hamburguer.setter
     def hamburguer(self, hamburguer):
         self._hamburguer = hamburguer
    
     @property
     def soda(self):
         return self._soda
    
     @soda.setter
     def soda(self, soda):
         self._soda = soda
    
    
     def add_potato(self, quantity, size):
         self.potato = Potato(quantity, size)
        
     def add_hamburguer(self, quantity, size):
         self.hamburguer = Hamburguer(quantity, size)
    
     def add_soda(self, quantity, size):
         self.soda = Soda(quantity, size)
    
     def show_order(self):
             print(
                 f'Customer Name: {self.customer_name}\n'\
                 f'Burger: {self.hamburguer.quantity}| {self.hamburguer.size}\n'\
                 f'Potato: {self.potato.quantity}| {self.potato.size}\n'\
                 f'Soda: {self.soda.quantity}| {self.soda.size}\n'\
                 f'Request Time: {start_request_time}\n'
                 f'Time ready: {final_order_time}\n'
             )

```
The composition concept is one of the most interesting to use, through a class, in our requested case it will be in the position of a base class, outside this class I will define other classes in the case Potato, Hamburgue, Soda where your definitions could be used by the base class(Order). This is very good, because you avoid adding many parameters to the class. In an organized way, each feature will be isolated. I will be able to compose part by part. As in the order example below.

```python
request1 = Request(
     customer_name='Rorismaldo Cruz',

)

request1.add_potato(2, 'medium')
request1.add_hamburguer(2, 'medium')
request1.add_soda(2, 'medium')
request1.show_order()

# output: 
# Customer Name: Rorismaldo Cruz
# Burger: medium| 2
# Potato: medium| 2
# Soda: medium| 2
# Request Time: 2023-05-21 15:10:29
# Time ready: 2023-05-21 15:50:29
```
```python
request1 = Request(customer_name='Mericliendes Bento')

request1.add_potato(8, 'small')
request1.add_hamburguer(2, 'big')
request1.add_soda(3, 'small')
request1.show_order()

# output: 
# Customer Name: Mericliendes Bento
# Burger: big| 2
# Potato: small| 8
# Soda: small| 3
# Request Time: 2023-05-21 15:10:29
# Time ready: 2023-05-21 15:50:29
```

-----

### Heritage

Inheritance brings a major point to code reuse. Using the example below of a Car class, we pass its characteristics in the example the model of the car. Then we declare a new class like Micro_Car, Sedans, Sports_Car that are borrowing from the Car class its characteristic. Then we can instantiate a new object using these classes.

```python
# parent class
class Car:

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


    def print_car(self):
        print("")

#child class
class Micro_Car(Car):

    def print_car(self):
        print(f"Your microcar is : {self.modelo}")

#child class
class Sedans(Car):

    def print_car(self):
        print(f"Your sedan is: {self.modelo}")

#child class 
class Sports_Cars(Car):

    def print_car(self):
        print(f"Your Sport Car is: {self.modelo}")

#We create the object
bond_bug = Micro_Car("Bond Bug")
fiat_cinquecento= Sedans("Fiat Cinquecento")
porsche_911= Sports_Cars("Porsche 911")


bond_bug.print_car()
fiat_cinquecento.print_car() 
porsche_911.print_car() 

# output:
# Your microcar is : Bond Bug
# Your sedan is: Fiat Cinquecento
# Your Sport Car is: Porsche 911
```

-----

### Polymorphism

Polymorphism is another object-oriented concept, how can we modify a subclass method that has its definition in the parent class. In this way we reuse our already defined methods, through inheritance. Modifying method structures in subclasses

```python

# Parent class
class Time_Activite:
    def time(self):
        pass

# Child class
class Total_Time_kilimeter(Time_Activite):
    
    def __init__(self, total_kilimeter, time_input):
        self.total_kilimeter = total_kilimeter
        self.time_input = time_input
    
    # Method modification
    def time(self):
        
        #Note that the results are different, being able to create the same methods more for different behaviors
        return f'Your activite => Kilimeter: {self.total_kilimeter} | Time: {self.time_input}'

# Child class
class Time_Per_Kilimeter(Time_Activite):

    def __init__(self, total_kilimeter, time_input):
        self.total_kilimeter = total_kilimeter
        self.time_input = time_input
    
    # Method modification
    def time(self):
        time = (self.time_input * 60)
        time = (time / self.total_kilimeter)
        time = (time / 60)
        
        #Note that the results are different, being able to create the same methods more for different behaviors
        return f'Your time per kilometer: {time} minutes'
    
    
results = [Total_Time_kilimeter(5, 20), Time_Per_Kilimeter(7, 35)]

for result in  results:
    print(result.time())

# output:
# Your activite => Kilimeter: 5 | Time: 20
# Your time per kilometer: 5.0 minutes
```

-----

### Interface

The interface in the Python language does not have a structure like the Java language for this type of implementation. It is normal to be considered as example structure using attributes and methods, but it lacks its strict validation. See the Running and Mountain Bike classes below. These classes are getting in the example class from an Interface_Sport interface. Using the activate_activity function you can take advantage of the interface declared using the start_activite method.

```python
class InterfaceSport:
    def start_activite(self):
        pass
    
class Run(InterfaceSport):
    def start_activite(self):
        return "Race start, let's go !!!"
    
class MountainBike(InterfaceSport):
    def start_activite(self):
        return "MTB start, let's go !!!"
    
def activate_activity(activity):
    print(activity.start_activite())
    
run = Run()
mtb = MountainBike()

activate_activity(run)
activate_activity(mtb)

# output:
# Race start, let's go !!!
# MTB start, let's go !!!
```

## Comments

Thanks for reading this far. I hope I can help you understand. Any code or text errors please do not hesitate to return. Don’t forget to leave a like so you can reach more people.

## Resources

Here I left the notebook of the codes above. If you have never used jupyter notebook. I recommend downloading the file and uploading it to Google drive and then opening it with Google Colab. Clicking to run you will avoid creating an environment and soon you will be able to test the code.

- [Repository — OOP](https://github.com/sc0v1n0/article_oop)

## About the author:

{% embed https://dev.to/sc0v0ne %}

**A little more about me...**

Graduated in Bachelor of Information Systems, in college I had contact with different technologies. Along the way, I took the Artificial Intelligence course, where I had my first contact with machine learning and Python. From this it became my passion to learn about this area. Today I work with machine learning and deep learning developing communication software. Along the way, I created a blog where I create some posts about subjects that I am studying and share them to help other users.

I'm currently learning TensorFlow and Computer Vision

Curiosity: I love coffee

- [Kaggle](https://www.kaggle.com/sc0v1n0)
- [Gitlab](https://gitlab.com/sc0v0n3)
- [Github](https://github.com/sc0v0ne)
- [Mastodon](https://mastodon.social/@sc0v0ne)