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.
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
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
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.
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.
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.
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
order1 = Order('Severius', 'X-Bacon','To the point', False, False)
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
order2 = Order('Kratos', 'X-Fish','To the point', True, True)
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.
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'
car.turn_on_car()
#output: 'Car On'
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.
class House:
def __init__(self, color):
self.color = color
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.
class House:
def __init__(self, color):
self._color = color
def print_color(self):
print(f'Color this house is {self._color}')
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.
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.
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.
class House:
def __init__(self, color):
self.__color = color
def print_color(self):
print(f'Color this house is {self.__color}')
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.
class Car:
def __init__(self, model, motor):
self.model = model
self.__motor = motor
car = Car('Ferrari', 'V8')
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.
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')
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.
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)
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
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.
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.
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.
kitchen.order = request1
print(kitchen.order.deliver_kitchen_order())
# output: Order number: 1671
#Customer Name: Rorismaldo Cruz
#Hamburg: X-All
#Note: No onion
kitchen = Kitchen('Crilzancar', 'Morning')
print(kitchen.boss_kitchen, kitchen.time)
# output: Crilzancar Morning
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
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.
# 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
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.
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.
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.
# 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
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.
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
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.
# 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
# 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.
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.
About the author:
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