Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Python Decorator Usage and Best Practices

Tech Sep 25 1

How to Use Function Decorators

A common implementation of Fibonacci:

def fibonacci(n):
    if n <= 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

This approach, typical of recursion in C, leads to massive redundant calculations. For example, computing fibonacci(10) requires fibonacci(8) and fibonacci(9), and computing fibonacci(9) requires fibonacci(7) and fibonacci(8).

An improved version with caching:

def fibonacci(n, cache=None):
    if cache is None:
        cache = {}
    if n in cache:
        return cache[n]
    if n <= 1:
        return 1
    cache[n] = fibonacci(n - 1, cache) + fibonacci(n - 2, cache)
    return cache[n]

if __name__ == '__main__':
    print(fibonacci(50))

The result is computed instantly.

Using a decorator:

def memo(func):
    cache = {}
    def wrap(n):
        if n not in cache:
            cache[n] = func(n)
        return cache[n]
    return wrap

@memo
def fibonacci(n):
    if n <= 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

if __name__ == '__main__':
    print(fibonacci(50))

How to Preserve Function Metadata After Decoration

Functions store metadata such as:

  • f.__name__: the function name
  • f.__doc__: the docstring
  • f.__module__: the module name
  • f.__dict__: attribute dictionary
  • f.__defaults__: default parameter tuple

After applying a decorator, accessing these attributes reveals the wrapper function's metadata, causing the original function's metadata to be lost.

Solution:

  • Use update_wrapper
  • Use wraps
from functools import wraps

def mydecorator(func):
    @wraps(func)
    def wrapper(*args, **kargs):
        """wrapper function"""
        print('In wrapper')
        func(*args, **kargs)
    return wrapper

@mydecorator
def example():
    """example function"""
    print('In example')

if __name__ == '__main__':
    example()
    print(example.__name__)

How to Define Decorators with Arguments

Practical Case:

  • Create a decorator that validates the types of the decorated function's parameters.
  • The decorator accepts arguments specifying parameter types. If a mismatch is detected at call time, an expection is raised.

@type_assert(str, int, int)<br></br>def f(a, b, c):<br></br> ...<br></br><br></br>@type_assert(y=list)<br></br>def g(x, y):<br></br> ...

Solution:

  • Extract the function signature: inspect.signature()
  • A decorator with arguments is essentially a factory that produces a specific decorator. Each call to type_assert returns a custom decorator to apply to other functions.
import inspect

def type_assert(*ty_args, **ty_kwargs):
    def decorator(func):
        func_sig = inspect.signature(func)
        bind_type = func_sig.bind_partial(*ty_args, **ty_kwargs).arguments

        def wrap(*args, **kwargs):
            for name, obj in func_sig.bind(*args, **kwargs).arguments.items():
                type_ = bind_type.get(name)
                if type_:
                    if not isinstance(obj, type_):
                        raise TypeError('%s must be %s' % (name, type_))
            return func(*args, **kwargs)
        return wrap
    return decorator

@type_assert(c=str)
def f(a, b, c):
    pass

if __name__ == '__main__':
    f(5, 10, 's')  # passes validation
    f(5, 10, 1)    # fails validation: 1 is not a string

How to Create a Decorator with Mutable Attributes

Practical Case:

In a project with performance issues, implement a decorator with a timeout parameter to analyze function execution times:

  • Log the execution time of each function call.
  • If the time exceeds the timeout value, log the call details.
  • The timeout value should be modifiable at runtime.

@warn_timeout(1.5)<br></br>def func(a, b):<br></br> ...

Solution:

  • Add a function to the wrapper to modify the free variable used in the closure. In Python 3, use nonlocal to reference variables in the enclosing scope.
import time
import logging

def warn_timeout(timeout):
    def decorator(func):
        def wrap(*args, **kwargs):
            t0 = time.time()
            res = func(*args, **kwargs)
            used = time.time() - t0
            if used > timeout:
                logging.warning('%s: %s > %s', func.__name__, used, timeout)
            return res

        def set_timeout(new_timeout):
            nonlocal timeout
            timeout = new_timeout

        wrap.set_timeout = set_timeout
        return wrap
    return decorator

import random

@warn_timeout(1.5)
def f(i):
    print('in f [%s]' % i)
    while random.randint(0, 1):
        time.sleep(0.6)

for i in range(3):
    f(i)

f.set_timeout(1)
for i in range(3):
    f(i)

How to Define Decorators Within a Class

Practical Case:

  • Implement a decorator that logs function call details to a file.
  • Record the call time, execution time, and call count for each function.
  • Group decorated functions to log to different files.
  • Allow dynamic modification of parameters, such as log format.
  • Enable toggling log output on and off.

Solution:

  • Use an instence method of a class as a decorator. The wraper function can then hold a reference to the instance, making it easier to modify attributes and extend functionality.
import time
import logging

DEFAULT_FORMAT = '%(func_name)s -> %(call_time)s\t%(used_time)s\t%(call_n)s'

class CallInfo:
    def __init__(self, log_path, format_=DEFAULT_FORMAT, on_off=True):
        self.log = logging.getLogger(log_path)
        self.log.addHandler(logging.FileHandler(log_path))
        self.log.setLevel(logging.INFO)
        self.format = format_
        self.is_on = on_off

    def info(self, func):
        _call_n = 0

        def wrap(*args, **kwargs):
            func_name = func.__name__
            call_time = time.strftime('%x %X', time.localtime())
            t0 = time.time()
            res = func(*args, **kwargs)
            used_time = time.time() - t0
            nonlocal _call_n
            _call_n += 1
            call_n = _call_n
            if self.is_on:
                self.log.info(self.format % locals())
            return res
        return wrap

    def set_format(self, format_):
        self.format = format_

    def turn_on_off(self, on_off):
        self.is_on = on_off

import random
ci1 = CallInfo('mylog1.log')
ci2 = CallInfo('mylog2.log')

@ci1.info
def f():
    sleep_time = random.randint(0, 6) * 0.1
    time.sleep(sleep_time)

@ci1.info
def g():
    sleep_time = random.randint(0, 8) * 0.1
    time.sleep(sleep_time)

@ci2.info
def h():
    sleep_time = random.randint(0, 7) * 0.1
    time.sleep(sleep_time)

for _ in range(3):
    random.choice([f, g, h])()

ci1.set_format('%(func_name)s -> %(call_time)s\t%(call_n)s')
for _ in range(3):
    random.choice([f, g])()

Tags: Python

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.