python-course.eu

8. Functional Programming OOP

By Bernd Klein. Last modified: 09 Mar 2024.

When working with higher-order functions, especially in the context of decorators, we often encounter the need to make inner states or objects of our function visible or accessible from the outside. In our decorator examples, we have already observed that by using closures, inner objects were created that we couldn't access externally. This was intentional or the desired effect in many cases. However, what if we want to expose these inner states externally? In this chapter, we want to discuss various ways to achieve this. Essentially, we can achieve this "window to the outside" through attribution or through complex return objects such as tuples or dictionaries. We have used both techniques before, but here we aim to refine this approach.

zebra lion hybrid

Example

The following call_counter decorator tracks the number of calls to a function. This aids in performance analysis, optimization, or debugging by providing a simple mechanism to monitor and log function invocations, enabling developers to understand usage patterns and identify potential areas for improvement.

For the purpose of this chapter of our tutorial the inner function get_calls is of special interest. The get_calls function provides a convenient way to retrieve and access the count of function calls externally. The get_calls function within the call_counter decorator behaves similarly to a getter method in a class. It encapsulates the logic for accessing the value of the calls variable, providing a way to retrieve this value from outside the decorator. This design pattern aligns with the principles of encapsulation and abstraction commonly used in object-oriented programming, where getter methods are used to access the internal state of an object while maintaining data integrity and hiding implementation details. In this case, get_calls acts as a getter function for the calls variable, providing a clean and controlled way to retrieve its value externally.

def call_counter(func):
    
    calls = 0
    def helper(*args, **kwargs):
        nonlocal calls
        calls += 1
        return func(*args, **kwargs)
    
    def get_calls():   
        return calls
    
    helper.get_calls = get_calls

    return helper

from random import randint

@call_counter
def f1(x):
    return 3*x

@call_counter
def f2(x, y):
    return x + 3*y

for i in range(randint(100, 3999)):
    f1(i)
    
for i in range(randint(100, 3999)):
    f2(i, 32)
    
print(f"{f1.get_calls()=}, {f2.get_calls()=}")

OUTPUT:

f1.get_calls()=691, f2.get_calls()=3622

Thus, we have only a read-only access to the counter from outside through the get_calls function. If, for any reason, one desires to allow external modification of the counter – even though it may not seem very sensible – this can be easily achieved with an additional function called set_calls.

We will go a step further in the following section and show how functional programming techniques can indeed be used to mimic aspects of class design. In Python, functions and closures can be leveraged to encapsulate state and behavior similar to how classes do.

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here

Object Oriented Programming (OOP) and Functional Programming

Object-oriented programming (OOP) and functional programming (FP) are among the most widely used programming paradigms. OOP focuses on using objects to represent data and behavior, while FP aims to design programs by combining functions for data transformation. Higher-order functions are a powerful concept in functional programming. They allow the abstraction and composition of code by treating functions as "first-class citizens," enabling manipulation like any other data type. Purely syntactically, one might get the impression that both programming paradigms are entirely different.

But if you look at the following implementation, you might get the impression that somebody wanted to write a class and just made an error by writing def in front of Person instead of class:

def Person(name, age):

    def self():
        return None
    
    def get_name():
        return name

    def set_name(new_name):
        nonlocal name
        name = new_name

    def get_age():
        return age
        
    def set_age(new_age):
        nonlocal age
        age = new_age

    self.get_name = get_name
    self.set_name = set_name
    self.get_age = get_age
    self.set_age = set_age

    return self

# Create a person object
person = Person("Russel", 25)

print(person.get_name(), person.get_age())

person.set_name('Jane')
print(person.get_name(), person.get_age())

OUTPUT:

Russel 25
Jane 25

The above program defines a function Person that, in a sense, behaves like a class definition. One can instantiate "Person" objects using Person. Within Person, there is a function named person, serving as a container for other inner functions. In a proper class definition, these inner functions would be referred to as methods. Similar to a class, we have defined getters (get_name and get_age) and setters (set_name and set_age). The Person function creates a closure for the local variables name and age, preserving their state. Using nonlocal, inner functions can access them. To access inner functions externally, we attach them as attributes to self, returned by Person.

In the following program, we define a corresponding "proper" class definition:

class Person2():

    def __init__(self, name, age):
        self.set_name(name)
        self.set_age(age)
    
    def get_name(self):
        return self.__name

    def set_name(self, new_name):
        self.__name = new_name

    def get_age(self):
        return self.__age
        
    def set_age(self, new_age):
        self.__age = new_age

# Create a Person2 object
person = Person2("Russel", 25)

print(person.get_name(), person.get_age())
person.set_name('Jane')
print(person.get_name(), person.get_age())

OUTPUT:

Russel 25
Jane 25

We extend our "functional class" function by adding a repr and a equal function (method in class terminology):

def Person(name, age):
    
    def self():
        return None

    def get_name():
        return name

    def set_name(new_name):
        nonlocal name
        name = new_name

    def get_age():
        return age

    def set_age(new_age):
        nonlocal age
        age = new_age

    def repr():
        return f"Person(name={name}, age={age})"

    def equal(other):
        nonlocal name, age
        return name == other.get_name() and age == other.get_age()

    self.get_name = get_name
    self.set_name = set_name
    self.get_age = get_age
    self.set_age = set_age
    self.repr = repr
    self.equal= equal

    return self
# Create a Person2 object
person = Person("Russel", 25)

print(person.get_name(), person.get_age())
person.set_name('Eve')
print(person.get_name(), person.get_age())

OUTPUT:

Russel 25
Eve 25
person2 = Person("Jane", 25)
person3 = Person("Eve", 25)
print(person.equal(person2))

OUTPUT:

False
print(person.equal(person3))

OUTPUT:

True
person.repr()

OUTPUT:

'Person(name=Eve, age=25)'
type(person)

OUTPUT:

function

Mimicking Inheritance

It's truly fascinating! This approach offers the flexibility to simulate inheritance or even multiple inheritance. By leveraging the power of higher-order functions, we can mimic the behavior of inheritance without explicitly defining classes. This purely functional approach opens up new possibilities for structuring and organizing our code, offering a unique perspective on object-oriented concepts.

Let us first define our class-like function again.

def Person(name, age):
    
    def self():
        return None

    def get_name():
        return name

    def set_name(new_name):
        nonlocal name
        name = new_name

    def get_age():
        return age

    def set_age(new_age):
        nonlocal age
        age = new_age

    def repr():
        return f"Person(name={name}, age={age})"

    def equal(other):
        nonlocal name, age
        return name == other.get_name() and age == other.get_age()


    methods = ['get_name', 'set_name', 'get_age', 'set_age', 'repr', 'equal']
    # creating attributes of the nested function names to self
    for method in methods:
        self.__dict__[method] = eval(method)

    return self
# Create a Person2 object
person = Person("Russel", 25)

print(person.get_name(), person.get_age())
person.set_name('Eve')
print(person.get_name(), person.get_age())

OUTPUT:

Russel 25
Eve 25

Employee is supposed to behave like a child class of Person:

from functools import wraps

def Employee(name, age, stuff_id):

    self = Person(name, age)
    # all attributes of Person are attached to self:
    self = wraps(self)(self)

    def get_stuff_id():
        return stuff_id

    def set_stuff_id(new_stuff_id):
        nonlocal stuff_id
        stuff_id = new_stuff_id

    # adding 'methods' of child class
    methods = ['get_stuff_id', 'set_stuff_id']
    for method in methods:
        self.__dict__[method] = eval(method)


    return self
    
x = Employee('Homer', 42, '007')
x.get_age()

OUTPUT:

42
x.set_stuff_id('434')
x.get_stuff_id()

OUTPUT:

'434'

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Upcoming online Courses

Enrol here

Exercises

Exercise 1

Create a Python class called Librarycatalogue that represents a catalogue for a library. The class should have the following attributes and methods:

Attributes:

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here

Methods:

Your task is to implement the Librarycatalogue class with the specified attributes and methods. Then, create instances of the class and test its functionality by adding books, removing books, finding books by author, finding authors by book, and displaying the catalogue.

Exercise 2

Rewrite the previous class as a function

Solutions

Solution to Exercise 1

class Librarycatalogue:
    def __init__(self):
        self.books = {}

    def add_book(self, title, author):
        self.books[title] = author

    def remove_book(self, title):
        if title in self.books:
            del self.books[title]
            print(f"Book '{title}' removed from the catalogue.")
        else:
            print(f"Book '{title}' is not in the catalogue.")

    def find_books_by_author(self, author):
        found_books = [title for title, auth in self.books.items() if auth == author]
        return found_books

    def find_author_by_book(self, title):
        if title in self.books:
            return self.books[title]
        else:
            return f"Author of '{title}' is not found in the catalogue."

    def display_catalogue(self):
        print("catalogue:")
        for title, author in self.books.items():
            print(f"- {title} by {author}")


# Test the Librarycatalogue class
library = Librarycatalogue()

# Add books to the catalogue
library.add_book("1984", "George Orwell")
library.add_book("To Kill a Mockingbird", "Harper Lee")
library.add_book("Ulysses", "James Joyce")

# Display the catalogue
library.display_catalogue()

# Find books by author
print("\nBooks by Harper Lee:", library.find_books_by_author("Harper Lee"))

# Find author by book
print("\nAuthor of '1984':", library.find_author_by_book("1984"))
print("Author of 'Ulysses':", library.find_author_by_book("Ulysses"))

# Remove a book
library.remove_book("To Kill a Mockingbird")

# Display the catalogue again
library.display_catalogue()

OUTPUT:

catalogue:
- 1984 by George Orwell
- To Kill a Mockingbird by Harper Lee
- Ulysses by James Joyce

Books by Harper Lee: ['To Kill a Mockingbird']

Author of '1984': George Orwell
Author of 'Ulysses': James Joyce
Book 'To Kill a Mockingbird' removed from the catalogue.
catalogue:
- 1984 by George Orwell
- Ulysses by James Joyce

Solution to Exercise 2

def Librarycatalogue():

    books = {}
    
    def self():
        return None

    # names of the nested functions to be exported
    methods = ['add_book', 'remove_book', 'find_books_by_author', 
                'find_author_by_book', 'display_catalogue']

    def add_book(title, author):
        books[title] = author

    def remove_book(title):
        if title in books:
            del books[title]
            print(f"Book '{title}' removed from the catalogue.")
        else:
            print(f"Book '{title}' is not in the catalogue.")

    def find_books_by_author(author):
        found_books = [title for title, auth in books.items() if auth == author]
        return found_books

    def find_author_by_book(title):
        if title in books:
            return books[title]
        else:
            return f"Author of '{title}' is not found in the catalogue."

    def display_catalogue():
        print("catalogue:")
        for title, author in books.items():
            print(f"- {title} by {author}")

    # creating attributes of the nested function names to self
    for method in methods:
        self.__dict__[method] = eval(method)

    return self

    
# Test the Librarycatalogue class
library = Librarycatalogue()

# Add books to the catalogue
library.add_book("1984", "George Orwell")
library.add_book("Hotel New Hampshire", "John Irving")
library.add_book("Ulysses", "James Joyce")

# Display the catalogue
library.display_catalogue()

# Find books by author
print("\nBooks by Harper Lee:", library.find_books_by_author("Harper Lee"))

# Find author by book
print("\nAuthor of '1984':", library.find_author_by_book("1984"))
print("Author of 'Ulysses':", library.find_author_by_book("Ulysses"))

# Remove a book
library.remove_book("To Kill a Mockingbird")

# Display the catalogue again
library.display_catalogue()

OUTPUT:

catalogue:
- 1984 by George Orwell
- Hotel New Hampshire by John Irving
- Ulysses by James Joyce

Books by Harper Lee: []

Author of '1984': George Orwell
Author of 'Ulysses': James Joyce
Book 'To Kill a Mockingbird' is not in the catalogue.
catalogue:
- 1984 by George Orwell
- Hotel New Hampshire by John Irving
- Ulysses by James Joyce

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Upcoming online Courses

Enrol here