Top Rated Plus on Upwork with a 100% Job Success ScoreView on Upwork
retzdev logo
logo
tech

Decorators and Partially Applied Functions in Python

by Jarrett Retz

February 20th, 2024 typescript functional programming python decorator

Introduction

I recently changed someone else's Python code in a way that I thought fixed a problem. However, after showing them the change, they provided context that moved me to revert the commit and find a different way to handle the potential Exception.

In my quest to elegantly handle the error in a functional and non-evasive way, I started on a path that took me to a very pleasing software development destination!

What am I talking about? I discovered (while perusing SO) the partial function in the functools module of Python's built-in library. And with this function, I created a re-usable error handling decorator that fits my use case nicely.

In this article, I'll share why using the partial function to help me build a decorator was precisely the tool I needed.

Functional Programming with Typescript

In the last year and a half, I've become enamored with functional programming-inspired libraries in Typescript (TS):

Understanding functional programming terms like closure, higher-order function, and partially applied function is important to using the libraries effectively. Consequently, I became accustomed to these tools and started reaching for them in my work with Python.

Back to Python

In the scenario presented earlier, I modified a Python function to handle a ZeroDivisonError. However, later, I was told the ZeroDivisonError only happens when the user makes a mistake. Therefore, instead of handling this error and hiding the user's mistake, we should catch it and alert the user that they may be doing something undesirable.

Now that I knew I needed to catch the error, and not prevent it, I no longer needed to change the function that someone else had written. Instead, I wanted to wrap it in a try and except block so I could present a custom error message to the user.

Higher Order Functions

I started searching for how higher-order functions work in Python and came across an article on freeCodeCamp by Roy Chng titled Python Decorators Explained For Beginners.

"Decorators give you the ability to modify the behavior of functions without altering their source code, providing a concise and flexible way to enhance and extend their functionality."

Roy Chng, Python Decorators Explains for Beginners

Perfect, wrapping a function is something I'm very comfortable with from my knowledge of TS and higher-order functions. However, I still wanted to check on how Python defines decorators:

"A function returning another function usually applied as a function transformation using the @wrapper syntax. Common examples for decorators are classmethod() and staticmethod(). [...] The decorator syntax is merely syntactic sugar [...]"

Python Docs, decorator

The examples given in Python's documentation are instructive: classmethod() and staticmethod().

# https://docs.python.org/3/glossary.html#term-decorator

def f(arg):
    ...
f = staticmethod(f)

@staticmethod
def f(arg):
    ...

I want to pass the other programmer's function to my function and execute it inside a try and except block. The decorator function and usage may look something like the code below:

def zero_division_error(calc_func):
    def inner(*args, **kwargs):
        try:
            return calc_func(*args, **kwargs)
        except ZeroDivisionError as e:
            # Possibly log error
            raise ValueError('Your other inputs were invalid') from e
    return inner
    
    
@zero_division_error
def calc(x, y):
  return x / y

print(calc(10, 0))
# ZeroDivisionError: division by zero
# ValueError: Your other inputs were invalid

This code is certainly good enough. However, I want more. Instead of returning the default ZeroDivisionError message, I want the function to return a custom message passed in by the developer. Consequently, I'll need to find a way of passing the error message argument to the decorator.

I know that passing arguments to decorators can be done because many libraries have decorators that do this (thinking of FastAPI).

# FastAPI example API route
@router.put(
    "",
    status_code=status.HTTP_201_CREATED,
    dependencies=[Depends(get_current_active_user)],
)
def create(value: Value):
  # handle PUT request

First things first, let's discuss why this is useful and how closures are helpful.

Closures

"A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time."

MDN WebDocs, Closure

Previously, while using TS, closures have been a way to save a variable until later use, a way to "preload" a reusable function with some targeted information. Initially, I wasn't sure I could use closures in Python because I had not seen the syntax (despite having used closures unknowingly with FastAPI).

In TS, writing a closure may look like:

const multiply = (x: number) => (y: number) => y * x

const hoursToMinutes = multiply(60)

const minutesToSeconds = multiply(60)

// hoursToMinutes(5)
// 300

But you can't do this (in the same way) in Python. Still, adding the lexical (closure) scope to a function can be very useful in building flexible software functions.

So, how can I write this function to only take a function as its last argument but also take other arguments before that?

Partial

In my search, I found this SO answer that solved my problems. The answer gives the example:

# srj
# https://stackoverflow.com/questions/5929107/decorators-with-parameters/25827070#25827070

from functools import partial

def _pseudo_decor(fun, argument):
    def ret_fun(*args, **kwargs):
        #do stuff here, for eg.
        print ("decorator arg is %s" % str(argument))
        return fun(*args, **kwargs)
    return ret_fun

real_decorator = partial(_pseudo_decor, argument=arg)

@real_decorator
def foo(*args, **kwargs):
    pass

Above, the example introduces the partial function that is part of the functools module in Python. With the partial function, I can provide my custom error message before passing in any arbitrary function (hopefully a mathematical one).

Then, the decorator function can access the error message in the closure (lexical) scope.

from typing import Union
from functools import partial

def custom_zero_division_error(calc_func, errorMessage: Union[None, str] = None):
    def inner(*args, **kwargs):
        try:
            return calc_func(*args, **kwargs)
        except ZeroDivisionError as e:
          if errorMessage is not None:
            # Provide custom message, keeping stack trace from ZeroDivisionError
            raise ValueError(errorMessage) from e
          else:
            # Re-raise error but keep stack trace
            raise
    return inner

real_decorator = partial(custom_zero_division_error, errorMessage='One of your inputs is a valid type, but is a duplicate of the previous value, which may be zero.')

@real_decorator
def calc(x, y):
  return x / y

print(calc(10, 0))
# ZeroDivisionError: division by zero
# ValueError: One of your inputs is a valid type, but is a duplicate of the previous value, which may be zero.

If you start chaining higher-order functions together you may have a situation where a function returns two functions before returning a value. Referring back to the TS example, multiply(60) is a partially applied function because it's still waiting for the second number to multiply by. That's where the partial function in Python draws its name.

More Decorator Examples with (better) Explanations

The SO answer links to a PyCon talk where the presenter explains decorators nicely. He also works through five examples of decorator usage in Python.

It's a helpful video and worth the watch. I was excited to see one of the examples use memoization, which I had just written about in my last article, albeit in TS.

Conclusion

Programming paradigms in one language show up in other languages. Learning about functional programming techniques in TS helped me navigate an often confusing topic for Python developers.

It reminds me of an advertisement I saw at a gas station on the digital display once, however, I'm not sure if it's a perfect fit. Anyway, it read, "All roads lead to more roads, and that's pretty cool."

Jarrett Retz

Jarrett Retz is a freelance web application developer and blogger based out of Spokane, WA.

jarrett@retz.dev

Subscribe to get instant updates

Contact

jarrett@retz.dev

Legal

Any code contained in the articles on this site is released under the MIT license. Copyright 2024. Jarrett Retz Tech Services L.L.C. All Rights Reserved.