Python decorators - a quick introduction
Published: 2024-01-06 23:37:36

A decorator is a function that takes a function as input and returns another function, adding functionalities or replacing the entire fuction itself.
 It is applied when the function is defined.

```python
from functools import wraps
def timethis(func):
    ''' this decorator reports execution time '''
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end -start)
        return result
    return wrapper    

```

According to the[ official documentation](https://docs.python.org/3/library/functools.html), `@functools.wraps` is a convenience function for invoking `update_wrapper()` as a function decorator when defining a wrapper function. 
If you forget to use `@wraps`, the decorated function will lose its name, docstring, annotations and calling signature.
Using `@wraps` , we can inject code to be executed alongside a given function, without losing function metadata.
 In the given example, we start counting time before executing the original function, then stopping when it returns and print the time interval. 

```python
from time import sleep

@timethis
def timer(seconds):
    ''' creat a timer'''
    time.sleep(seconds)
```

## Defining a more useful decorator taking arguments (mandatory or optional)

Let's code a logging decorator to our timer function

```python
import logging
def logged(level, name = None, message = None):
     ''' add logging '''
     def decorate(func):
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__
        @wraps(func)
        def wrapper(*args,**kwargs):
            log.log(level, logmsg)
            return func(*args, **kwargs)
        return wrapper
     return decorate

```

Decorating the function
```python
@logged(logging.CRITICAL, 'example')
def timer(seconds):
    sleep(seconds)
```

In this example, we have a decorator taking our custom arguments and then returning a decorator. This decorator then is applied to the function we want to decorate.



```python
def logged(func = None, *, level = logging.DEBUG, name = None, message = None):
     ''' add logging '''
     if func is None:
         return partial(logged, level=level, name=name, message=message)
        
     def decorate(func):
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__
        @wraps(func)
        def wrapper(*args,**kwargs):
            log.log(level, logmsg)
            return func(*args, **kwargs)
        return wrapper
     return decorate
```
To create a decorator that takes no arguments, and avoid the calling conventions between decorators without any arguments and decorators with arguments, we use [partial](https://docs.python.org/3/library/functools.html) to return a new partial object which when called behaves like func called with the positional arguments args and keyword.

## Can a decorator be defined as a class? 
Also, decorators can be defined as classes and can be applied to class and static methods.

In fact, a decorator can be defined as a class, like [@property](https://docs.python.org/3/library/functions.html?highlight=property#property), with a get, a set and a del method.

There are a lot more about decorators.
To really understand how decorators work you need to understand the variable scoping, closures, mtaclasses and descriptors.
I strong recommend you to read [Fluent Python](https://www.oreilly.com/library/view/fluent-python-2nd/9781492056348/) and [Python Cookbook](https://www.oreilly.com/library/view/python-cookbook-3rd/9781449357337/).