If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Tim Peters, Zen of Python
Before we talk about programming paradigms, we must first define what a program is: a set of instructions in a language that can be translated into machine code and executed by a computer. As long as we don't make any algorithmic mistakes and our instructions don't require unnecessary computation, it doesn't really matter how exactly we construct these programs for the computer to execute. However, since computer programs are (so far) written by humans, we need to structure our code and keep it in an understandable form so that it can be maintained and extended by humans. As programs have become more complex, three basic programming paradigms have emerged: Procedural, Object-Oriented, and Functional Programming. These are ways of thinking about solving problems and structuring program code. There are programming languages designed specifically for one of these paradigms (such as Haskell for functional programming). Python, which we use here, is in principle suitable for all three paradigms, which is why we call Python a multi-paradigm programming language. What all three paradigms have in common is that they modularize code, but they use different concepts to do so. This commonality is not surprising, since any large program will contain code that needs to be executed many times in many places. Writing this code over and over again would be redundant, whereas it would be elegant to write the code as generally as possible once, and then call it in different places.
Before we introduce the paradigms, however, we need to be clear about two types of programming: imperative and declarative. Imperative programming means that we give the computer precise step-by-step instructions that it then executes, which is ultimately the type of programming we have been learning so far. The program code explicitly expresses how the instructions should be executed. Declarative programming, on the other hand, is less about how and more about what. The code expresses what should happen rather than exactly how. Imperative programming is closer to the way a computer works, so imperative code is easier for the machine to translate. Procedural and object-oriented programming follow the imperative approach. Declarative programming is used less often and is more abstract.
Procedural programming is an imperative paradigm in which programs are modularized into procedures. Procedures (or subroutines) in Python are typically implemented as functions. For example, Jupyter Notebooks, a widely used application for statistics and data science, often uses procedural programming. Procedural programming is initially more intuitive and easier to learn for beginners. Smaller programs can be written in a flexible way. Unfortunately, procedurally written code is difficult to transfer to other projects, and the code is rarely truly reusable.
# Managing a Flower Store: Procedural Approach
# Store flowers
flowers = {}
def add_flower(flower_type, quantity):
if flower_type in flowers:
flowers[flower_type] += quantity
else:
flowers[flower_type] = quantity
def get_quantity(flower_type):
return flowers.get(flower_type, 0)
# Adding flowers
add_flower("Roses", 10)
add_flower("Lilies", 15)
# Getting quantity of a flower type
print("Roses available:", get_quantity("Roses"))
As you can see, procedural programming is a straightforward approach that focuses on writing sequences of
instructions, or procedures, to operate on data. In our florist example, we define a global dictionary of
flowers and a set of functions, such as add_flower
and get_quantity
,
to manage it. The add_flower
function adds a specific quantity of a flower type
to the inventory, while get_quantity
retrieves the quantity of a given flower
type. This approach emphasizes a clear, linear flow of execution and is effective in scenarios where tasks
can be broken down into simple, reusable steps. It's easy to follow because it reflects how we might write a
series of steps in plain language.
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects," which can contain data and methods that manipulate that data. The goal of OOP is to organize program code into modular parts. If we think of a computer program as an organism, different functions are divided into organs. Staying with the example, the heart has the function of pumping blood, but how it does this is not relevant to the liver (oversimplified). This corresponds roughly to the basic ideas of object-oriented programming, so it is not surprising that the first object-oriented programming language, Simula, was developed in the 1960s to simulate natural systems.
# Managing a Flower Store: Object-Oriented Approach
class FlowerStore:
def __init__(self):
self.flowers = {}
def add_flower(self, flower_type, quantity):
if flower_type in self.flowers:
self.flowers[flower_type] += quantity
else:
self.flowers[flower_type] = quantity
def get_quantity(self, flower_type):
return self.flowers.get(flower_type, 0)
# Creating a FlowerStore instance
flower_store = FlowerStore()
# Adding flowers
flower_store.add_flower("Roses", 10)
flower_store.add_flower("Lilies", 15)
# Getting quantity of Roses
print("Roses available:", flower_store.get_quantity("Roses"))
Object-oriented programming (OOP) focuses on creating objects that encapsulate both data and methods. In our
OOP example, the florist software is modeled as a FlowerStore
class that
encapsulates the inventory in the flowers
attribute and provides methods such as
add_flower
and get_quantity
for interacting with
this data. This encapsulation makes OOP a powerful tool for modeling real-world entities and managing their
interactions. It allows more complex structures and behaviors to be represented in a way that is consistent
with our real-world understanding of objects, providing a clear structure and organization to the code. The
use of classes and objects also facilitates code reuse and scalability.
As the name implies, Functional programming is based on the use of functions. At first, this sounds a lot like procedural programming, but it is fundamentally different because functional programming follows the declarative approach. The basic element of functional programming is the use of pure functions. Pure functions are functions that can only access their input and produce output from it, and do nothing else, i.e., they have no side effects. A pure function cannot affect any other variable in the program. Therefore, pure functions will predictably always produce the same output for a given input, regardless of the context. This doesn't sound very special at first, but the consequences of this restriction on the construction of functions and the resulting power become apparent only after some use.
# Managing a Flower Store: Functional Approach
def add_flower(flowers, flower_type, quantity):
updated_flowers = flowers.copy()
updated_flowers[flower_type] = updated_flowers.get(flower_type, 0) + quantity
return updated_flowers
def get_quantity(flowers, flower_type):
return flowers.get(flower_type, 0)
# Initial empty flower collection
flowers = {}
# Adding flowers in an immutable way
flowers = add_flower(flowers, "Roses", 10)
flowers = add_flower(flowers, "Lilies", 15)
# Getting quantity of Roses
print("Roses available:", get_quantity(flowers, "Roses"))
Functional programming treats computation as the evaluation of mathematical functions and avoids changing
states and mutable data. In the functional version of the florist software, operations are performed by pure
functions such as add_flower
and get_quantity
. These
functions take an input (the current state of the inventory) and return a new state without modifying the
original data, following the principle of immutability. This approach improves predictability and
maintainability because the functions are "pure" and have no side effects. It's particularly useful in
scenarios that require high consistency, because it ensures that the same input will always produce the same
output. Functional programming is also beneficial for parallel processing because immutable data reduces
data conflict issues.
Programs with the same functionality can be built using different programming paradigms. AI can help you choose the appropriate paradigm, experiment with it, and write code in a particular paradigm. For example, instead of asking, "How do I structure my program?", ask, "Can you show me how to build an inventory system in Python using procedural, object-oriented, and functional programming?" In this way, AI can provide examples and guide you through implementing the same functionality using different paradigms, allowing you to compare and choose the best approach for your needs.
Example Prompt:Resulting AI-generated code:
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def __str__(self):
return f"{self.title} by {self.author} ({self.year})"
class Library:
def __init__(self):
self.books = []
def add_book(self, book):
self.books.append(book)
def get_books(self):
return self.books
# Creating book instances
book1 = Book("1984", "George Orwell", 1949)
book2 = Book("To Kill a Mockingbird", "Harper Lee", 1960)
# Creating a Library instance
library = Library()
# Adding books to the library
library.add_book(book1)
library.add_book(book2)
# Getting all books in the library
for book in library.get_books():
print(book)