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 wrapperThen 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 resultsThen 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 clsWe 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.