Skip to main content
  1. Posts/

Python Debugging with Decorators

·544 words·3 mins
Table of Contents

Decorators
#

Often times while writing python code you may encounter a logical error, one that won’t be caught in compilation or execution, but rather during run time. Perhaps something didn’t execute or return as expected, or maybe a function executed and returned the anticipated result, but took an astounding long time to do it.

Test driven development will catch these kind of issues as we work, but traditionally a lot of people will just throw a whole lot of print statements throughout their code. This could create more issues that we’re fixing.

Depending on your python experience you may or may not have encountered a decorator. Decorators allow you to wrap and modify a function to modify it’s behaviour. If you’ve used libraries like flask or django, you will have already experienced the power of decorators:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

We can take advantage of decorators to add a simple mechanism to help us to debug our code. Here’s a simple decorator that will catch the input and output arguments of a function, and the time it took to execute:

import functools
import time
from loguru import logger

LOG_FILENAME = "Project_{time:YYYY-MM-DD}.log"
logger.add(LOG_FILENAME, format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}", rotation="1 days", retention="2 months", compression="zip", enqueue=True)
logger.remove(0)

def logwork(*, entry=True, exit=True, level="DEBUG"):
    def wrapper(func):
        name=func.__name__

        @functools.wraps(func)
        def wrapped(*args, **kwargs):
            logger_ = logger.opt(depth=1)
            if entry:
                logger_.log(level, "Entering '{} (args={}, kwargs={})", name, args, kwargs)
            start = time.time()
            result = func(*args, **kwargs)
            end = time.time()
            logger.debug("Function '{}' executed in {:f} s", name, end-start)
            if exit:
                logger_.log(level, "Exiting '{} (result={})", name, result)
            return result
        return wrapped
    return wrapper

Then during our debugging we can simply wrap the functions that we’re interested in tracking:

@logwork
def simplefunction(val1: int, val2: str, val3: dict) -> dict:
    val3['age'] = val1
    val3['name'] = val2
    return val3

@logwork
def complexfunction(results: list, length: int) -> list:
    for item in length:
        results.append(simplefunction())
    return results

Then wafter executing we’ll see something like the following in our logs:

Entering simplefunction (args=(val1, val2, val3), kwargs=(10,'bob',{'data':'example'}))
Function simplefunction executed in 0.3 s
Exiting simplefunction (result=({'data':'example','age':10,'name':'bob'}))

Immutability
#

Python classes can dynamically have attributes added or changed, we can again use decorators to prevent a class being updated or changed (or having additional variables added).

Below is a decorator that will do exactly that. In this case it looks for attributes that aren’t present, but we could select an existing attribute as well:

def immutable(cls):
    cls.__frozen = False

    def frozensetattr(self, key, value):
        if self.__frozen and not hasattr(self, key):
            print("Class {} is frozen. Cannot set {} = {}"
                  .format(cls.__name__, key, value))
        else:
            object.__setattr__(self, key, value)

    def init_decorator(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            func(self, *args, **kwargs)
            self.__frozen = True
        return wrapper

    cls.__setattr__ = frozensetattr
    cls.__init__ = init_decorator(cls.__init__)

    return cls

We can see it in action by creating a new class with a variable. This existing variable we can amend and edit, but we can’t create new variables (like you normally can in python). If you try it’ll print the statement about it being frozen.

@immutable
class Foo(object):
    def __init__(self):
        self.bar = 10

foo = Foo()
foo.bar = 42
foo.foobar = "no way"   

You can take it further still, and apply it to concepts like Monkey Patching, such as this example from Guido van Rossum.