DEV Community

Cover image for FP In Python: Is It Genuinely Functional?
Mahmoud Harmouch
Mahmoud Harmouch

Posted on • Edited on

FP In Python: Is It Genuinely Functional?

If you have followed me on Twitter, you may have noticed that I tweeted something about FP and OOP being the same in Python. I later realized that most Pythonistas didn't get the idea I was trying to deliver. Therefore, I decided to write an article to give you a whole rundown of my recent discoveries in Python.

Edit: This post was published months ago, I was editing it, and forget to publish the edits. For some reason, I removed it from publication, not sure why. Therefore, this is just a re-upload of an already published post.

πŸ‘‰ Table Of Contents (TOC).

Introdcution.

πŸ” Go To TOC.

Object-oriented programming and functional programming are not the same thing, and they are two different paradigms with their own advantages and disadvantages. However, the division between the two paradigms has been blurred in python since everything is modeled as an object stored in a heap [0]. In fact, it turned out that everything you can do with classes, such as inheritance, and composition, can also be achieved with functions in python: aka callable objects.

To give you an idea of what I'm referring to, let's go over an overview of the two paradigms.

What is Object-Oriented Programming?

πŸ” Go To TOC.

Object-oriented programming is a programming paradigm that represents the concept of objects as data structures that contain data, aka states, and methods, aka behaviors. It is a way of designing programs by breaking them down into smaller parts called objects, which can be used to model real-world entities like animals, cars, etc. The object-oriented approach to programming has been around since the 1960s, and it has been popularized by languages such as Java, C++, Python, Ruby, etc.

Since this article is tied only to the python language, let's take the following listing to model an animal with a behavior. For simplicity reasons, let's assume that an animal has only two primary properties: a name and a color. Additionally, we can use the attrs package to avoid boilerplate code(e.g., __init__) in our class.

from attrs import define

@define
class Animal:
  name: str
  color: str
  def run(self) -> None:
    print(f"An animal whose name: {self.name} and color: {self.color} is currently running!")
Enter fullscreen mode Exit fullscreen mode

In python 2, the definition of that class would be slightly different: class Animal(object). Since we are using python 3, every class inherits from the object class by default. Moreover, objects can be created, manipulated, and destroyed. The object's data can be accessed using the object's name, a reference variable, followed by the dot operator, called "dot notation.". The same applies to the object's methods.

animal = Animal("An animal", "purple")
print(animal.name)
animal.run()
animal.color = "blue"
del animal
Enter fullscreen mode Exit fullscreen mode

Running the previous snippet of code will result in the following being displayed on your terminal:

An animal
An animal whose name: An animal and color: purple is currently running!
Enter fullscreen mode Exit fullscreen mode

This programming paradigm uses inheritance to create inheritance hierarchies. An object can be viewed as a template for the data and behavior of an object. Object-oriented programming is practical when dealing with complex data structures.

Now let's model a specific type of animal: A Dog which, as you may know, can be achieved with the power of inheritance.

from attrs import define

@define
class Dog(Animal):
  def run(self) -> None:
    print(f"A dog whose name: {self.name} and color: {self.color} is currently running!")
Enter fullscreen mode Exit fullscreen mode

As noted earlier, an object can be created, manipulated, and destroyed. States and behaviors of an object can be accessed using dot notation.

dog = Dog("Finn", "white")
dog.run()
dog.color = "blue"
del dog
Enter fullscreen mode Exit fullscreen mode

Running the previous listing will result in the following being displayed on your terminal:

A dog whose name: Finn and color: white is currently running!
Enter fullscreen mode Exit fullscreen mode

You can even do multilevel inheritance.

from attrs import define

@define
class Puppy(Dog):
  def run(self) -> None:
    print(f"A puppy whose name: {self.name} and color: {self.color} is currently running!")
Enter fullscreen mode Exit fullscreen mode

In addition to that, you can do composition; you get the point.

from attrs import define

@define
class Puppy(Animal, object):
  def run(self) -> None:
    print(f"A puppy whose name: {self.name} and color: {self.color} is currently running!")
Enter fullscreen mode Exit fullscreen mode

Now, let's move on to the functional programming paradigm to see if we can achieve the same logic with functions.

What is Functional Programming?

πŸ” Go To TOC.

Functional programming is a programming paradigm that treats computation to evaluate mathematical functions and avoids side-effects and mutable data. It means that this paradigm does not use variables to store information. Instead, it uses immutable values, which cannot be changed after they are created. It is a declarative paradigm, which means that the program's flowchart consists of statements such as f(x) = a.x + b.

The main difference between functional and object-oriented programming is that functional programs are more concerned with mathematical functions, and other types of computations. In contrast, object-oriented programs are more concerned with objects that have states and behaviors.

Now let's explore this programming paradigm by transforming our previous examples into functions.

def animal(name: str, color: str) -> None:
  def run():
    print(f"An animal whose name: {name} and color: {color} is currently running!")
  return run
Enter fullscreen mode Exit fullscreen mode

Until now, everything looks as usual. But our animal callable object is missing attributes which begs the question: Is it doable to do so in python? And the answer is yes, ever since pep-232 [1].

def run() -> None:
  global animal
  print(f"An animal whose name: {animal.name} and color: {animal.color} is currently running!")

def animal():
  ...
Enter fullscreen mode Exit fullscreen mode

Now, we can define attributes and methods for the animal callable object as follows:

animal.name = "An animal"
animal.color = "blue"
animal.run = run
Enter fullscreen mode Exit fullscreen mode

Now, we can call the inner callable object as shown in the listing below:

animal.run()
Enter fullscreen mode Exit fullscreen mode

Running the previous snippets of code will result in the following being displayed on your terminal:

An animal whose name: An animal and color: blue is currently running!
Enter fullscreen mode Exit fullscreen mode

We can even modify the inner attributes as shown below:

animal.name = "Another animal"
animal.run()
del animal
Enter fullscreen mode Exit fullscreen mode

Which results in:

An animal whose name: Another animal and color: blue is currently running!
Enter fullscreen mode Exit fullscreen mode

With that noted, you start to see a considerable similarity between both animal objects created using a class or function(not a function, just a callable object). Now, this begs the question: is it possible to do inheritance using functions. And the answer is yes, with the help of python decorators.

def run() -> None:
  global animal
  print(f"An animal whose name: {animal.name} and color: {animal.color} is currently running!")

def animal(func):
  def inner(*args, **kwargs):
    return func(*args, **kwargs)
  return inner

animal.name = "An animal"
animal.color = "blue"
animal.run = run

@animal
def dog():
  ...
Enter fullscreen mode Exit fullscreen mode

Now, we can define attributes and methods for the dog callable object as follows:

def run():
  global dog
  print(f"A dog whose name: {dog.name} and color: {dog.color} is currently running!")

dog.name = "Finn"
dog.color = "blue"
dog.run = run
Enter fullscreen mode Exit fullscreen mode

Now, we can call the inner callable object as shown in the listing below:

dog.run()
Enter fullscreen mode Exit fullscreen mode

Output:

A dog whose name: Finn and color: blue is currently running!
Enter fullscreen mode Exit fullscreen mode

We can even modify the inner attributes as shown below:

dog.name = "Katie"
dog.run()
del dog
Enter fullscreen mode Exit fullscreen mode

Output:

A dog whose name: Katie and color: blue is currently running!
Enter fullscreen mode Exit fullscreen mode

However, the effect of the decorator is entirely demolished because we didn't do much in the inner function. To illustrate the mechanism of the inner function, we can call the run function of the callable animal object.

def run() -> None:
  global animal
  print(f"An animal whose name: {animal.name} and color: {animal.color} is currently running!")

def animal(func):
  def inner(*args, **kwargs):
    animal.run()
    return func(*args, **kwargs)
  return inner

animal.name = "An animal"
animal.color = "blue"
animal.run = run

@animal
def dog() -> None:
  ...
Enter fullscreen mode Exit fullscreen mode

Now, instantiating the object dog will execute the run function of the callable animal object:

dog()
Enter fullscreen mode Exit fullscreen mode

Output:

An animal whose name: An animal and color: blue is currently running!
Enter fullscreen mode Exit fullscreen mode

You can even do multilevel inheritance like the following:

def run() -> None:
  global animal
  print(f"An animal whose name: {animal.name} and color: {animal.color} is currently running!")

def animal(func):
  def inner(*args, **kwargs):
    animal.run()
    return func(*args, **kwargs)
  return inner

animal.name = "An animal"
animal.color = "blue"
animal.run = run

def run():
  global dog
  print(f"A dog whose name: {dog.name} and color: {dog.color} is currently running!")

@animal
def dog(func):
  def inner(*args, **kwargs):
    dog.run()
    return func(*args, **kwargs)
  return inner

dog.name = "Finn"
dog.color = "blue"
dog.run = run

@dog
def puppy() -> None:
  ...

puppy()
Enter fullscreen mode Exit fullscreen mode

Output:

An animal whose name: An animal and color: blue is currently running!
A dog whose name: Finn and color: blue is currently running!
Enter fullscreen mode Exit fullscreen mode

At this point, it seems like I lost my damn mind. You know what, screw it. You can even do composition with decorators by chaining them together:

def run() -> None:
  global animal
  print(f"An animal whose name: {animal.name} and color: {animal.color} is currently running!")

def animal(func):
  def inner(*args, **kwargs):
    animal.run()
    return func(*args, **kwargs)
  return inner

animal.name = "An animal"
animal.color = "blue"
animal.run = run

def run() -> None:
  global dog
  print(f"A dog whose name: {dog.name} and color: {dog.color} is currently running!")

def dog(func):
  def inner(*args, **kwargs):
    dog.run()
    return func(*args, **kwargs)
  return inner

dog.name = "Finn"
dog.color = "blue"
dog.run = run

@animal
@dog
def puppy() -> None:
  ...

puppy()
Enter fullscreen mode Exit fullscreen mode

Running the previous snippet of code will result in the following output:

An animal whose name: An animal and color: blue is currently running!
A dog whose name: Finn and color: blue is currently running!
Enter fullscreen mode Exit fullscreen mode

Holy smokes! What even is this? Am I just tripping, or the whole language is a complete mess? This results in me questioning my existence.

That was a bit dramatic. It is just the language giving you a lot of flexibility to invent whatever the hell you can think of. But, there is a significant trade-off here because it would make pure functional programming impossible to do in python. It would also result in huge memory consumption by making everything objects. Python is a weird programming language.

Conclusion.

πŸ” Go To TOC.


memegenerator.net.

Objects in python are inevitable. You may think that you are doing pure functional programming, but it turns out it is not the case. You are just using callable objects to build your logic which takes space on the heap, which is not the usual approach unlike other programming languages that uses a stack to allocate frames for functions.

I am not sure why Guido thought making everything inherit from the object class was a good idea cause, as you may know, it will consume loads of ram to run a relatively complex python program.

In conclusion, purely functional programming is entirely unfeasible to do in python. Bear in mind that the purpose of this article is not to bash the language itself but rather to point out what I think are fundamental flaws in the design of the language; not sure if this issue can be fixed because the language has evolved drastically over the years.

It is also quite important to mention that, in theory, building relatively complex web frameworks based on python will result in high memory consumption, again due to the fact that everything in python is just an object.

Key takeaway:

  • There is no such thing as a function in python. Instead, You can only use objects to create a callable object which is not a pure function.

There is a lot to discuss on this topic regarding performance, scalability, maintainability, etc. Presumably, I will investigate it even further whenever I have the time to do so. In the meantime, I am looking forward to your responses. What do you think? Is there any significant difference between the two paradigms in python?

That's all for today's article. Thank you for reading! Stay hydrated, folks; see you in the next one.

Image by Gerd Altmann from Pixabay

References & Resources.

πŸ” Go To TOC.

[0] docs.python.org, python 3.10.4. Memory Management.

[1] Barry Warsaw, 2000. PEP 232 – Function Attributes.

[2] Wikipedia, the free encyclopedia. Object-oriented programming.

[3] Wikipedia, the free encyclopedia. Functional programming.

Top comments (0)