"Object-oriented programming: the blueprint for building digital worlds, where data and behavior unite in elegant real world objects"
- Intro to OOP
- Class VS Instance Attributes and Methods
- Class vs Instance methods + @staticmethod
- Key Paradigms of OOP
- Examples of good OOP designs
- Quiz
- Homework
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of objects, which can contain data and code.
There are two main classifiers which object could have attributes and methods. Let's take a closer look on them!
Classifier | Attributes | Methods |
---|---|---|
Definition | Attributes represent the state or qualities of the object, often called fields or properties. | Methods are functions defined inside an object. They represent the behavior or actions that an object can perform. |
Usage | Used to store information about the object, like size, color, or other properties. | Used to define actions that can be performed by the object, like calculations, operations, or any other functions. |
Data is represented and structured in the following format. Below you can see the attributes
and methods
defined for the dict
class.
print(dir(dict))
['clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
# clear -> method (because it's an operation on the object)
# 'copy', -> method
# 'fromkeys' -> method
# 'get', -> method
# ...
# 'values' -> attribute (field of the object) -> returns all values of the dictionary
# 'items', -> attribute
# 'keys', -> attribute
There are lots of examples can be found just around the world. Let's take a look at car.
What color is it? What is the maximum speed? These are all - properties of the object.
The car can drive, break, stall and rev. These are - methods of the object.
Let's take a look at the table below with more examples:
Real-World Object | Attributes | Methods |
---|---|---|
Car | color , brand , horsepower , fuel_level |
drive() , brake() , stall() , rev() |
Bank Account | account_number , balance , account_holder |
deposit(amount) , withdraw(amount) , check_balance() |
Smartphone | model , operating_system , battery_percentage , screen_size |
call(number) , send_message(content) , take_photo() |
Book | title , author , number_of_pages , genre |
read_page(page_number) , bookmark_page(page_number) , close() |
Same it can be represented in the world of programming. You simply create the class
, define its attributes
and its methods
. It comes in handy, when you can have a custom object
for specific needs.
In Python
, classes are defined using the class
keyword, followed by the class's name and a colon. Inside the class, methods
(functions) are defined to implement the behaviors of the objects.
Note: Take a look at indentation, in case it's wrong the Pyhton
interpter will consider method as a function.
class Car:
def __init__(self, color, brand): # params
self.color = color # attributes
self.brand = brand
def drive(self): # method of the class
print("This car is now driving.")
def brake(self): # method of the class
print("The car has stopped.")
car_1 = Car('brown', 'bugatti') # create a specific instance of the class (Calling __init__() under the hood)
car_2 = Car('blue', 'volvo')
# Attributes can be accessed with a ``.(dot)`` notation, same as we've seen before with dictionary
print(f"Andrew Tate owns a {car_1.color} {car_1.brand}")
print(f"Average person owns a {car_1.color} {car_1.brand}")
# Methods as well
car_1.drive()
Andrew Tate owns a brown bugatti
Average person owns a brown bugatti
This car is now driving. # Buggati is driving, while Volvo is stationary for now.
-
We created a class
Car
with the following attributescolor
andbrand
, 2 instances of thisclass
- (car_1
,car_2
) and called methoddrive()
for the first instance. -
self
- specific instance of the class. By usingself
, we can access the attributes and methods of the class inPython
. It binds the attributes with the given arguments. -
__init__(*args, **kwargs)
- is a special method that's automatically called when a newinstance (object)
of aclass
is created. It is also know asconstructor
method.
Consider self
as reference to specific object, for example we are all people, but each person (self
) is unique.
print(id(car_1))
print(id(car_2))
140094732495696
140094732495760
Note: Calling __init__()
initialising the newly created object's attributes with specific values, which we pass while creating an object.
Note: car_1
and car_2
are completly different objects, although both of them are instances of the same class, this can be prooved by using id()
function.
Now you can see that it is very convinient way to store some data with Python classes, as you don't have to define keys and values like you did it using dictionary.
class Book:
def __init__(self, title, author, is_borrowed=False):
self.title = title
self.author = author
self.is_borrowed = is_borrowed
def borrow_book(self):
if not self.is_borrowed:
self.is_borrowed = True
return f"You have borrowed '{self.title}' by {self.author}." # Yes, you can use this
else:
return f"'{self.title}' is already borrowed."
def return_book(self):
if self.is_borrowed:
self.is_borrowed = False
return f"'{self.title}' has been returned."
else:
return f"'{self.title}' was not borrowed."
# Creating instances of ``Book``
book_1 = Book("1984", "George Orwell")
book_2 = Book("To Kill a Mockingbird", "Harper Lee")
# Borrowing the 1st book
print(book_1.borrow_book())
# Attempting to borrow the 1st book again
print(book_1.borrow_book())
# Borrowing the 2nd book
print(book_2.borrow_book())
# Returning the first book
print(book_1.return_book())
# Checking the status of the second book
print(f"Is '{book_2.title}' borrowed? {'Yes' if book_2.is_borrowed else 'No'}") # BTW, we can define a method for that inside the class, just use your imagination!
You have borrowed '1984' by George Orwell.
'1984' is already borrowed.
You have borrowed 'To Kill a Mockingbird' by Harper Lee.
'1984' has been returned.
Is 'To Kill a Mockingbird' borrowed? Yes
Once we have learnt about functional and object oriented programmming, there is often sort of confusion exists which to use among both paradigms.
Object-Oriented Programming (OOP)
is often chosen for large , complex systems where encapsulating data and behavior into objects makes the code more manageable, readable and reusable.
It is great for modeling real-world entities and is commonly used in software development for user interfaces
, simulations
, and large-scale applications
.
Functional Programming
is often used in data science world. Functions do specific things, but classes - are specific things.
Frankly speaking, in real world programming the majority of applications are written using OOP
, as it is a modern approach for high-level languages. But it's a good practice to combine both paradigms and use functions with classes in your application.
When working with classes in object-oriented programming
, it's crucial to understand the difference between class attributes vs instance attributes.
-
Instance attributes and methods are tied to a specific instance of a class.
-
Each instance has its own copy of these attributes and methods.
-
Changing an instance attribute only affects that particular instance, not all instances of the class.
- Use these when the value or behavior should be the same across all instances of the class.
- They are not tied to any particular instance of the class.
- If the class attribute value is changed, the change is reflected across all instances.
You can access the class attributes with several ways: Class.class_attiribute
or instance.class_attribute
.
class Vehicle:
total_vehicles = 0 # Class attribute
def __init__(self, make, model):
self.make = make # Instance attribute
self.model = model # Instance attribute
Vehicle.total_vehicles += 1
# Creating instances of Vehicle
car1 = Vehicle("Toyota", "Corolla")
car2 = Vehicle("Ford", "F-150")
# Accessing class attribute
print("Total vehicles:", Vehicle.total_vehicles)
# Accessing instance attributes
print(car1.make, car1.model)
print(car2.make, car2.model)
"""
# As it was mentioned before, the value and behavior is the same across all instances of the class.
# Class attributes and methods can be accessed through the instances of that class.
"""
print(car1.total_vehicles == car2.total_vehicles) # True
Total vehicles: 2
Toyota Corolla
Ford F-150
True
class User:
active_users = 0 # Class attribute
def __init__(self, username):
self.username = username # Instance attribute
User.active_users += 1
# Creating instances of ``User`` (calling __init__())
user1 = User("Alice")
user2 = User("Bob")
print(user1.active_users)
print(user2.active_users)
2
2
In each example, the class attribute
is shared among all instances of the class and reflects a property
or statistic
that is relevant to the class
. But outputing them using .(dot)
notation is not really comfortable way to manipulate the object
. There are some more efficient ways described below in section 3
.
In Python
methods within a class
can be categorized into three types based on their interaction with class
or instance
attributes / instance methods
, class methods
, and static methods
. Each type serves its unique purpose and is defined differently.
Definition: Functions defined inside a class that operate on instances of the class. They can freely access and modify instance attributes
and other instance methods
.
First Parameter: self
- refers to the individual instance of the class.
Usage: Used for operations that require data specific to an individual object .
class Vehicle:
def __init__(self, make, model):
self.make = make
self.model = model
# Instance method
def display_info(self):
return f"This vehicle is a {self.make} {self.model}."
car = Vehicle("Toyota", "Corolla")
print(car.display_info()) # Outputs: This vehicle is a Toyota Corolla.
Definition: Class methods
are functions defined inside a class that operate on the class itself
, rather than on instances of the class.
First Parameter cls
- refers to the class itself, not the instance.
Decorator: @classmethod
- indicates that the method is a class method.
Usage: Typically used for operations that apply to the class as a whole, rather than to individual objects.
class Vehicle:
total_vehicles = 0
@classmethod
def increment_total_vehicles(cls):
cls.total_vehicles += 1
Vehicle.increment_total_vehicles()
print(Vehicle.total_vehicles) # Outputs: 1
Definition: Static methods
are functions defined inside a class that don’t implicitly access either class
or instance attributes
.
Decorator: @staticmethod
- indicates that the method is a static method.
Usage: Typically used for utility functions that perform a task in isolation. They can't modify class or instance state.
class Vehicle:
@staticmethod
def is_motorcycle(wheels):
return wheels == 2
print(Vehicle.is_motorcycle(2)) # Outputs: True
print(Vehicle.is_motorcycle(4)) # Outputs: False
Note: Threre is no First Parameter
passed as an argument to the method.
Understanding when and how to use each method type is crucial for designing effective and logical classes and the whole system design.
class BankAccount:
# Class attribute
total_accounts = 0
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
# Increment the total accounts (NOTE: we can actually create a method which does this and call it in our constructor)
BankAccount.total_accounts += 1
# Instance method (for direct interaction with ``specific object``)
def deposit(self, amount):
self.balance += amount
return f"{self.owner}'s account: Deposited ${amount}. New balance: ${self.balance}."
# Instance method (for direct interaction with ``specific object``)
def withdraw(self, amount):
if amount > self.balance:
return f"{self.owner}'s account: Insufficient funds. Withdrawal denied."
self.balance -= amount
return f"{self.owner}'s account: Withdrew ${amount}. New balance: ${self.balance}."
# Class method to get total accounts
@classmethod
def get_total_accounts(cls):
return f"Total bank accounts opened: {cls.total_accounts}."
# Static method to check if a withdrawal amount is within a daily limit
@staticmethod
def check_daily_limit(amount, daily_limit=500):
return amount <= daily_limit
# Creating bank account instances
account1 = BankAccount("John Doe", 1000)
account2 = BankAccount("Jane Doe", 500)
# Calling instance methods
print(account1.deposit(500))
print(account2.withdraw(200))
# Calling class method
print(BankAccount.get_total_accounts())
# Calling static method
print(BankAccount.check_daily_limit(400))
print(BankAccount.check_daily_limit(600))
John Doe's account: Deposited $500. New balance: $1500.
Jane Doe's account: Withdrew $200. New balance: $300.
Total bank accounts opened: 2.
True
False
You can experiment with all methods described above and create powerful custom classes according to the rules.
There are several OOP concepts which every developer on the planet Earth must have to know .
Encapsulation
is often considered the first pillar of Object-Oriented Programming
. It refers to the bundling of data with the methods that operate on that data.
It is used to hide the internal representation, or state, of an object from the outside.
Let's explore Encapsulation
through a real-world example: a Coffee Machine
.
You (the customer) interact with it through a simple interface
:
- Selecting a type of coffee.
- Placing a cup.
- Pressing a button.
However, the internal processes are:
- Grinding beans .
- Heating water.
- Mixing coffee and water with a specific pressure and temperature.
This is encapsulation
in action - exposing only the necessary controls to the user and hiding the complex process.
In programming, encapsulation
is implemented through the use of private
/protected
/public
attributes
and methods
.
In simple words it is called the access levels:
-
Public Access: By default, all attributes and methods in a
Python
class
arepublic
. They can be easily accessed from outside the class. -
Protected Access
(_)
: This is more of a convention than enforced by the language. It signals that these attributes and methods should not be accessed directly, even thoughPython
does not strictly enforce this. -
Private Access
(__)
: Python performsname mangling
on these names. This means thatPython
interpreter changes the name of the variable in a way that makes it harder to createsubclasses
that accidentallyoverride
theprivate attributes
andmethods
.
The decent code which can represent Encapsulation
as a concept is the following: TODO
class Smartphone:
def __init__(self, brand, model):
self.brand = brand # Public attribute
self.model = model # Public attribute
self.__battery_level = 100 # Private attribute (initially fully charged)
self._installed_apps = [] # Protected attribute (initial app list)
def install_app(self, app_name):
"""Public method to install a new app."""
if app_name not in self._installed_apps:
self._installed_apps.append(app_name)
print(f"App '{app_name}' installed.")
else:
print(f"App '{app_name}' is already installed.")
def uninstall_app(self, app_name):
"""Public method to uninstall an app."""
if app_name in self._installed_apps:
self._installed_apps.remove(app_name)
print(f"App '{app_name}' uninstalled.")
else:
print(f"App '{app_name}' is not installed.")
def show_installed_apps(self):
"""Public method to display installed apps."""
print("Installed Apps:", self._installed_apps)
def __check_battery(self, required_amount):
"""Private method to check if enough battery is available."""
return self.__battery_level >= required_amount
def get_battery_level(self):
"""Public method to check the battery level."""
return f"Current battery level: {self.__battery_level}%"
# Creating and interacting with a Smartphone object
my_phone = Smartphone('Pixel', 'Pixel 5')
# Installing and uninstalling apps
my_phone.install_app('WhatsApp')
my_phone.install_app('Spotify')
my_phone.uninstall_app('WhatsApp')
my_phone.show_installed_apps()
# Using battery and charging
print(my_phone.get_battery_level())
my_phone.charge_phone(30)
Attribute/Method | Type | Access Level | Description |
---|---|---|---|
brand |
Attribute | Public | Can be accessed both inside and outside the class. |
model |
Attribute | Public | Can be accessed both inside and outside the class. |
__battery_level |
Attribute | Private | Can only be accessed and modified within the class. |
_installed_apps |
Attribute | Protected | Intended for internal use within the class or subclasses, but can technically be accessed from outside (Don't do that!) |
install_app(app_name) |
Method | Public | Can be accessed outside the class. |
uninstall_app(app_name) |
Method | Public | Can be accessed outside the class. |
show_installed_apps() |
Method | Public | Can be accessed outside the class. |
charge_phone(amount) |
Method | Public | Can be accessed outside the class. |
get_battery_level() |
Method | Public | Can be accessed outside the class. |
__check_battery(amount) |
Method | Private | Can only be accessed within the class. Checks if the battery level is sufficient for a specified operation. |
App 'WhatsApp' installed.
App 'Spotify' installed.
App 'WhatsApp' uninstalled.
Installed Apps: ['Spotify']
Current battery level: 100%
Basically, we created a good and solid interface for the user to interact with, moreover, we have hidden the techincal implementation from the user, which doesn't have to fully understand how it works inside, they would just use the Smartphone
straightforward.
class CoffeeMachine:
def __init__(self):
self.__water_level = 1000
self.__beans_quantity = 500
self.__temperature = 90
def make_coffee(self, coffee_type):
if not self.__check_resources(coffee_type):
return "Please refill the machine."
return f"Enjoy your {coffee_type}!"
def __check_resources(self, coffee_type):
# Private method to check if there are enough resources to make the coffee
if coffee_type == "espresso" and self.__water_level >= 50 and self.__beans_quantity >= 30:
self.__use_resources(50, 30)
return True
# Additional conditions for other coffee types can be added here
return False
def __use_resources(self, water_used, beans_used):
# Private method to use resources
self.__water_level -= water_used
self.__beans_quantity -= beans_used
def refill_water(self, water_quantity):
self.__water_level += water_quantity
def refill_beans(self, beans_quantity):
self.__beans_quantity += beans_quantity
# Interacting with the coffee machine
coffee_machine = CoffeeMachine()
print(coffee_machine.make_coffee("espresso")) # Enjoy your espresso!
coffee_machine.refill_water(500)
Enjoy your espresso!
The general usage:
-
Data Hiding The internal state of the coffee machine is hidden from the outside world. Users interact with a simple interface without worrying about the internal processes.
-
Access Modifiers: Users cannot directly access the internal components (like the water heater or the grinder); they can only use the buttons provided.
-
Simplicity: The user of the coffee machine doesn't need to know the exact process of how coffee is made. They only select the type of coffee they want and let the machine handle the rest.
-
Maintenance: If something goes wrong inside the coffee machine, or if an improvement is made to the internal mechanism (like a more efficient grinder), it doesn’t affect the user's interaction with the machine. The interface remains the same.
The @property
decorator in Python
allows you to define methods in a class that behave like attributes. But behind the scenes, it can perform complex computations, such as fetch data
, or implement logic with constraints
.
Note: If you also need to define a setter
function, you can use the @property_name.setter
decorator, where property_name
is the name of the property
.
class CarEngine:
def __init__(self):
self._rpm = 0 # Engine RPM, protected attribute
self._temperature = 70 # Engine temperature in Fahrenheit, protected attribute
@property
def rpm(self):
"""Get the current RPM of the engine."""
return self._rpm
@rpm.setter
def rpm(self, value):
"""Set the RPM of the engine, ensuring it's within a safe operational range."""
if 0 <= value <= 8000:
self._rpm = value
print(f"RPM set to {value}.")
else:
print("RPM must be between 0 and 8000.")
@property
def temperature(self):
"""Get the current temperature of the engine."""
return self._temperature
@temperature.setter
def temperature(self, value):
"""Set the temperature of the engine, ensuring it's within a safe operational range."""
if 50 <= value <= 250:
self._temperature = value
print(f"Temperature set to {value}°F.")
else:
print("Temperature must be between 50°F and 250°F.")
def start_engine(self):
"""Simulate starting the engine."""
if self._rpm == 0:
self.rpm = 1500 # Setting RPM to a typical idle speed.
print("Engine started.")
else:
print("Engine is already running.")
# Using the CarEngine class
my_car_engine = CarEngine()
my_car_engine.start_engine()
print(f"Current RPM: {my_car_engine.rpm}")
# Trying to set the RPM and temperature
my_car_engine.rpm = 9000
my_car_engine.temperature = 300
my_car_engine.rpm = 3000
my_car_engine.temperature = 200
RPM set to 1500.
Engine started.
Current RPM: 1500
RPM must be between 0 and 8000.
Temperature must be between 50°F and 250°F.
RPM set to 3000.
Temperature set to 200°F.
Use (Getters/Setters): If you need to expose internal state, do it through (getters
) and (setters
) to maintain control over the state. This allows for validation, logging, or other controls each time an attribute is accessed or modified.
Inheritance
allows us to define a class that inherits all the methods and properties from another class.
It's a way to form a relationship between classes, allowing for the creation of a more complex, yet organized system.
-
Code Reusability:
Inheritance
promotes the reuse of code. Once a behavior is defined in abase class
, it can be inherited by other classes, avoiding duplication. -
Extensibility: Modifications and enhancements can be made in the base class and all inheriting classes will automatically incorporate the changes. (Though in some cases it can violate
SOLID
, so be carefull with this) -
Hierarchy Creation: It helps in creating a hierarchical classification of classes which is natural in many real-world scenarios.
A child class inherits from one parent class. This is the simplest form of inheritance.
class Animal:
# Parent class code
class Dog(Animal):
# Child class Dog inherits from Animal
classDiagram
class Animal {
}
class Dog {
}
Animal <|-- Dog : inherits
A child class inherits from more than one parent class.
class LandAnimal:
# Code for land animals
class WaterAnimal:
# Code for water animals
class Frog(LandAnimal, WaterAnimal):
# Frog inherits from both LandAnimal and WaterAnimal
classDiagram
class LandAnimal {
}
class WaterAnimal {
}
class Frog {
}
LandAnimal <|-- Frog : inherits
WaterAnimal <|-- Frog : inherits
A class inherits from a child class, making it a grandchild class.
class Animal:
# Base class code
class Bird(Animal):
# Bird inherits from Animal
class Sparrow(Bird):
# Sparrow inherits from Bird, which is a child of Animal
classDiagram
class Animal {
}
class Bird {
}
class Sparrow {
}
Animal <|-- Bird : inherits
Bird <|-- Sparrow : inherits
IMPORTANT: In order to build a good quality codebase with inheritance, you, as a programmer should analyse what classes have in common and identify the highest level of Abstraction. TODO link to abstraction
For example Animal
and Dog
have the following in common. They both can walk()
, sound()
, eat()
. In this case, the Animal
is the highest level of Abstraction
.
super()
function is a crucial part of the inheritance mechanism. It allows you to call methods from a parent or sibling class
within a child class
.
class Book:
def __init__(self, title, author, ISBN):
self.title = title
self.author = author
self.ISBN = ISBN
print(f"Book '{self.title}' by {self.author} created. ISBN: {self.ISBN}")
def display_info(self):
print(f"Title: {self.title}, Author: {self.author}, ISBN: {self.ISBN}")
# Subclass ``EBook``
class EBook(Book):
def __init__(self, title, author, ISBN, file_format):
super().__init__(title, author, ISBN) # Initialize the base class attributes
self.file_format = file_format
print(f"EBook format: {self.file_format}")
def display_info_for_ebook(self):
super().display_info() # Display info from the base class method
print(f"File Format: {self.file_format}")
# Subclass ``PrintedBook``
class PrintedBook(Book):
def __init__(self, title, author, ISBN, weight):
super().__init__(title, author, ISBN) # Initialize the base class attributes
self.weight = weight
print(f"Printed Book weight: {self.weight} kg")
def display_info_for_printed_book(self):
super().display_info() # Display info from the base class method
print(f"Weight: {self.weight} kg")
ebook = EBook("The Python Journey", "Jane Doe", "123456789", "PDF")
ebook.display_info_for_ebook()
printed_book = PrintedBook("The Python Journey", "Jane Doe", "123456789", 1.5)
printed_book.display_info_for_printed_book()
Book 'The Python Journey' by Jane Doe created. ISBN: 123456789
EBook format: PDF
Title: The Python Journey, Author: Jane Doe, ISBN: 123456789
File Format: PDF
Book 'The Python Journey' by Jane Doe created. ISBN: 123456789
Printed Book weight: 1.5 kg
Title: The Python Journey, Author: Jane Doe, ISBN: 123456789
Weight: 1.5 kg
In this example, the EBook
and PrintedBook
classes inherit from the Book
class. They use super()
to initialize the attributes
from the Book
class and to extend the display_info
method.
This structure allows for shared behaviors and properties to be defined once in the Book
class while enabling each subclass to have its own specialized attributes and methods, maintaining a clear and organized hierarchy.
Alway try to follow these rules working with inheritance:
-
Ensure a Logical Hierarchy: The inheritance structure should reflect a logical and real-world hierarchy.
-
Avoid Deep Inheritance Trees: Deeply nested inheritance can lead to complexity and fragility in your code. Prefer composition over deep inheritance where possible.
Think in advance about the structure of inheritance and you will never have any problems with code, except of MRO
, heh, no spoilers!
Polymorphism
, is translated from the Greek words 'poly'
(many) and 'morph'
(form), is a cornerstone concept in Object-Oriented Programming
(OOP)
.
It refers to the ability of different objects to respond to the same message—or in programming terms, the ability of different classes to respond to the same method call—in their own unique ways.
We have encountered some functions
in Python
which use the same approach already:
print(len("Hello")) # Works on a string
print(len([1, 2, 3])) # Works on a list
Polymorphic Behavior: The len()
function returns the length of an object. It can be applied to various data types including strings
, lists
, tuples
, and dictionaries
.
How It Works: Internally, len()
calls the object's __len__
method. The implementation of __len__
can differ depending on the object's class , but from the user's perspective, len()
consistently returns the size or length of the object.
So technically speaking it is the mechanism which allows the same function or operator to work with different types of objects, but the behaivor remains unchanged.
Same is applied to min()
, max()
, +
, *
operators and functions.
- Code Reusability: Developers can write a function or method once and use it with objects of multiple classes.
- Flexibility in Code: New components are easy to integrate into the established system.
- Ease of Maintenance: As changes or enhancements are needed, developers can implement without the need to modify every implementation.
In method overriding
, a method in a child class
has the same name, return type, and parameters as a method in its parent class
.
The version of the method that gets executed depends on the type of the object invoking it.
class Animal:
def speak(self):
print("This animal speaks")
class Dog(Animal):
def speak(self): # Overrides the parental method (same name, same return type, same params)
print("Dog barks")
animal = Animal()
animal.speak()
dog = Dog()
dog.speak()
This animal speaks
Dog barks
class Vehicle:
def fuel_efficiency(self):
pass
class Car(Vehicle):
def fuel_efficiency(self):
return "Car: Approximately 30 miles per gallon."
class Truck(Vehicle):
def fuel_efficiency(self):
return "Truck: Approximately 15 miles per gallon."
# List of different types of vehicles
vehicles = [Car(), Truck()]
for vehicle in vehicles:
print(vehicle.fuel_efficiency())
Approximately 30 miles per gallon.
Approximately 15 miles per gallon.
Depending on the type of vehicle, the overridden method
provides specific fuel efficiency details.
There is one more type of polymorphism
called Operator Overloading
, but this will be explained during dunder(magic) methods
section, though we have seen and worked with it already, it always worth knowing what's under the hood.
Abstraction
in Object-Oriented Programming
(OOP
) is a conceptual mechanism that focuses on identifying the essential aspects of an entity while ignoring its detailed background and explanations.
Sounds hard, right? Let's take an example from the real world (like a smart TV)
When you use a smart TV, you don't need to know all the technical details about how TVs work internally.
Instead, you just use the remote control to switch it on, change channels, adjust the volume, and more. That means that you are using the highest level of abstraction and don't dive into the complex details.
"""
This can be a great example of how abstraction works, everything is hidden from the user and they still can use the player without having a look how it works inside
"""
class MusicPlayer:
def __init__(self, song):
self.song = song
# Assume there are complex operations to load and prepare the song
print(f"Loading the song: {self.song}")
def play(self):
# Complex operations to play music are hidden from the user
print(f"Playing the song: {self.song}")
def pause(self):
# Complex operations to pause the music are hidden
print(f"Pausing the song: {self.song}")
def stop(self):
# Complex operations to stop the music are hidden
print("Stopping the playback.")
# Using the MusicPlayer
my_music_player = MusicPlayer("Beethoven - Symphony No.9")
my_music_player.play()
my_music_player.pause()
my_music_player.stop()
It's all about exposing only the necessary parts of the entity/class, simplifying what the user needs to interact with.
Python
, being a dynamically-typed language, does not enforce abstraction
as strictly as statically-typed languages such as Java
or C++
.
Abstract classes
serve as blueprints for other classes
. They allow you to define methods that must be created (overriden) within any child classes
built from the abstract class
.
How to Implement:
-
abc Module
:Python
provides theabc (Abstract Base Classes)
module to define abstract base classes. -
@abstractmethod
Decorator: You can use the@abstractmethod
decorator to define abstract methods within an abstract class.
from abc import ABC, abstractmethod
class Shape(ABC): # Inherits from ABC, making Shape an abstract class.
"""
This class represent the highest level of abstraction.
You simply have to find what is common between it and its subsequent classes
This can be: area, perimeter, volume etc..
Same you can create the following hirearchy
``Animal`` is the highest level of abstraction, all animals have in common the following thinjgs such as: `speak()`, `eat()`, `drink()`, etc..
"""
@abstractmethod
def area(self):
pass # No implementation here. Subclasses MUST provide an implementation.
@abstractmethod
def perimeter(self):
pass # No implementation here. Subclasses MUST provide an implementation.
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
def perimeter(self):
return 2 * (self.length + self.width)
# rect = Shape() # This will raise an error, as you can't instantiate an abstract class.
rect = Rectangle(10, 20)
print(rect.area())
print(rect.perimeter())
200
60
Very important: An abstract method
is a method that has a declaration but does not have an implementation. But the specific implementation can be added for the methods in child classes based on the object
s` behaviour.
Abstraction is a powerful tool in software development, but like any tool, it must be used judiciously.
You have to use each principle of the Object Oriented Programming
wisely and consider everything in advance before taking an action.
The starting point is to understand the needs of your application and the users to define abstractions that accurately reflect real-world entities and operations.
"""
Imagine you're building an online store.
Initially, you might have a simple ``Product`` class. As the store grows, you recognize the need to categorize products.
!!! Instead of creating separate classes for each category with duplicated code, you abstract the common features into the `Product` class and use subclasses for specific behaviors. !!!
"""
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
class Book(Product):
def __init__(self, name, price, author):
super().__init__(name, price)
self.author = author
class Electronics(Product):
def __init__(self, name, price, warranty_period):
super().__init__(name, price)
self.warranty_period = warranty_period
# Usage
book = Book("The Pragmatic Programmer", 42.15, "Andy Hunt")
laptop = Electronics("SuperLaptop", 999.99, "1 year")
-
Understand the Problem Domain: The level of abstraction should match the complexity of the problem you're solving.
-
Avoid Over-Abstraction: Over-abstraction occurs when you create too many layers or overly generic(natural) structures, making the code hard to understand and maintain. If a simple function or class will do, there's no need to abstract further.
-
Prevent Under-Abstraction: Under-abstraction is when the code is too concrete, with repeated logic and lack of reusable components. Identify common patterns and behaviors in your code and abstract them into functions or classes.
Don't forget that you can refactor everything in the code no matter when and how it was written, but try your best to design a stable application during the planning stage!
Let's design a simple library management system and a zoo management system applying all the knowledge we have obtained during this lesson:
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
# Abstract class LibraryItem (Abstraction and Inheritance)
class LibraryItem(ABC):
def __init__(self, title, author, isbn):
self._title = title # Encapsulation: title is protected
self._author = author # Encapsulation: author is protected
self._isbn = isbn # Encapsulation: ISBN is protected
self._checkout_date = None
@abstractmethod
def checkout(self, days=14): # Abstract method (Abstraction)
self._checkout_date = datetime.now()
self._return_date = self._checkout_date + timedelta(days=days)
@abstractmethod
def get_return_date(self): # Abstract method (Abstraction)
if self._checkout_date:
return self._return_date
return "Item not checked out"
def get_details(self): # Encapsulation: Accessing protected attributes
return f"Title: {self._title}, Author: {self._author}, ISBN: {self._isbn}"
# Concrete class Book inheriting from LibraryItem (Inheritance)
class Book(LibraryItem):
def checkout(self, days=14): # Polymorphism: Method overriding
super().checkout(days) # Call to parent method
print(f"Book '{self._title}' checked out for {days} days.")
def get_return_date(self): # Polymorphism: Method overriding
return super().get_return_date()
# Concrete class DVD inheriting from LibraryItem (Inheritance)
class DVD(LibraryItem):
def checkout(self, days=7): # Polymorphism: Method overriding with different default days
super().checkout(days) # Call to parent method
print(f"DVD '{self._title}' checked out for {days} days.")
def get_return_date(self): # Polymorphism: Method overriding
return super().get_return_date()
# Usage (we use the list of objects here)
library_items = [
Book("The Pragmatic Programmer", "Andy Hunt", "123456789"),
DVD("The Matrix", "Lana Wachowski, Lilly Wachowski", "987654321")
]
for item in library_items:
print(item.get_details())
item.checkout()
print(f"Return Date: {item.get_return_date()}\n")
Title: The Pragmatic Programmer, Author: Andy Hunt, ISBN: 123456789
Book 'The Pragmatic Programmer' checked out for 14 days.
Return Date: [Calculated Date]
Title: The Matrix, Author: Lana Wachowski, Lilly Wachowski, ISBN: 987654321
DVD 'The Matrix' checked out for 7 days.
Return Date: [Calculated Date]
from abc import ABC, abstractmethod
# Abstract class Animal (Abstraction and Inheritance)
class Animal(ABC):
def __init__(self, name):
self._name = name # Encapsulation: name is protected
@abstractmethod
def speak(self): # Abstract method (Abstraction)
pass
@abstractmethod
def preferred_food(self): # Abstract method (Abstraction)
pass
def get_name(self): # Encapsulation: Accessing protected attribute
return self._name
# Concrete class Lion inheriting from Animal (Inheritance)
class Lion(Animal):
def speak(self): # Polymorphism: Method overriding
return f"{self.get_name()} the Lion: Roar!"
def preferred_food(self): # Polymorphism: Method overriding
return "meat"
# Concrete class Elephant inheriting from Animal (Inheritance)
class Elephant(Animal):
def speak(self): # Polymorphism: Method overriding
return f"{self.get_name()} the Elephant: Trumpet!"
def preferred_food(self): # Polymorphism: Method overriding
return "vegetation"
# Usage
zoo_animals = [
Lion("Leo"),
Elephant("Ella")
]
for animal in zoo_animals:
print(f"{animal.get_name()} speaks: {animal.speak()} and prefers {animal.preferred_food()}")
Leo speaks: Leo the Lion: Roar! and prefers meat
Ella speaks: Ella the Elephant: Trumpet! and prefers vegetation
Be creative with programming, try applying everything you have learnt with your custom classes and objects from the real world.
Remember that each application must have scalability, interactivity, readbility and usability. And here it is, now you are ahead of 90% of programmers!
What is the output of the following code?
class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return self.name + " says Woof!"
class Cat(Dog):
def speak(self):
return self.name + " says Meow!"
pet = Cat("Paws")
print(pet.speak())
A) Paws says Woof!
B) Paws says Meow!
C) An error occurs
D) None of the above
Which of the following is a principle of Object-Oriented Programming?
A) Recursion
B) Polymorphism
C) Synchronization
D) Multithreading
In Python, what does the
__init__
method do?
A) Initializes a new thread
B) Initializes a newly created object
C) Acts as a destructor for an object
D) None of the above
What does encapsulation mean in OOP?
A) Running multiple threads concurrently
B) Bundling of data with methods that operate on that data
C) Inheriting properties from a base class
D) Overriding methods in the child class
Which keyword is used to create a class in Python?
A) class
B) object
C) struct
D) def
How do you define a private attribute in a Python class?
A) By prefixing the attribute with an underscore _
B) By prefixing the attribute with two underscores __
C) By using the private
keyword
D) By defining the attribute outside of the class
What is inheritance in OOP?
A) The process by which an object acquires the properties of another object
B) The process by which a class acquires the properties and methods of another class
C) A function that a class performs
D) A type of function in mathematical programming
What does the term "polymorphism" refer to in OOP?
A) The ability of a function to perform different tasks based on the object
B) The ability of different classes to respond to the same function
C) The method of packing the data and functions together
D) The capability of a class to derive properties and characteristics from another class
Which of the following best describes an abstract class in OOP?
A) A class that cannot be instantiated and is designed to be subclassed
B) A class that provides a simple interface to a complex system
C) A template for creating objects
D) A class designed for efficient memory allocation
In Python, how do you define an abstract method?
A) By using the @abstract
decorator
B) By prefixing the method name with __
C) By using the @abstractmethod
decorator from the abc
module
D) By declaring the method in an interface
Which OOP principle is illustrated by defining a method in a child class that has the same name as a method in the parent class?
A) Encapsulation
B) Abstraction
C) Inheritance
D) Method Overriding
What is the output of the following code?
class A:
def speak(self):
return "Class A Speaks"
class B(A):
def speak(self):
return super().speak() + " and Class B Speaks"
obj = B()
print(obj.speak())
A) Class B Speaks
B) Class A Speaks and Class B Speaks
C) Class A Speaks
D) An error occurs
Which of the following statements about static methods in Python is true?
A) They can modify the state of an instance
B) They cannot access or modify the class state
C) They are declared using the @staticmethod
decorator
D) They must have at least one parameter
What does the
super()
function do in Python?
A) It returns the superclass of a class
B) It calls a method from the parent class
C) It initializes a superclass object
Objective: Create a virtual pet simulator where users can adopt a pet, feed it, play with it, and monitor its health and happiness.
- Implement a
Pet
class with attributes forname
,hunger
,happiness
, andhealth
. - Include methods to
feed
,play
, andcheck_status
of the pet. Feeding decreases hunger, playing increases happiness, and both actions affect health. - Create a simple user interface in the console to interact with the pet. (Try even using classes for
Menu
in the console itself)
Objective: Develop an interactive story game where choices made by the player lead to different outcomes. Use your imagination and create the whole story storing all nodes into a list afterall.
- Design a
StoryNode
class representing each point in the story, with a narrative part and choices leading to otherStoryNode
s. - Implement a method to display the story at the curent node and let the user make a choice.
- The game progresses based on the player's decisions until reaching an ending.
Objective: Implement a system to manage an art gallery, including artwork registration, artist information, and artwork sales.
- Create
Artist
andArtwork
classes, where eachArtwork
is associated with anArtist
. - The
Gallery
class should manage a collection of artworks, with methods to add artwork, sell artwork, and display available artworks. - Implement features to track the total sales and current inventory of the gallery.
- Use
polymorphism
,inheritance
,abstraction
andencapsulation
to organise your code.
Objective: Design a system to create, store, and display recipes, including ingredients, quantities, and cooking instructions.
- Define
Ingredient
andRecipe
classes, where aRecipe
can contain multipleIngredients
and their quantities. - The
Cookbook
class should aggregate multipleRecipe
s, with methods to add new recipes, find recipes by ingredient, and display all recipes. - Enhance user interaction with features like searching for recipes based on available ingredients.
Objective: Build a simple fitness tracker that allows users to log exercises, track workout routines, and monitor progress.
- Implement a
Workout
class with attributes likedate
,exercise
,duration
, andintensity
. - The
User
class should contain user details and a log of workouts, with methods to add a workout, summarize total activity, and set fitness goals. - Provide insights to the user based on their activity logs, such as total hours worked out in a month or progress towards goals.
- In the end, it would be cool to add several methods which calculate the calories burnt during workout sessions.