Python Decorators Advance
Dec 24, 2020
3 minute read

Accessing Wrapped Function Parameters In An Easy Way

import functools


def decorator(func):
    @functools.wraps(func)
    def wrap(*args, **kwargs):
        print(args, kwargs)
        return func(*args, **kwargs)

    return wrap

@decorator
def boo(a, b, c):
    pass

boo(1, b=2, c=3)
((1,), {'c': 3, 'b': 2})

The above codes is a general decorator function we always writen. The wrap inside the decorator uses (*args, **kwargs) as parameters for full compatibility with the wrapped func if we don’t know what parameters will be received.

import sys
import functools
import traceback


def decorator(func):
    @functools.wraps(func)
    def wrap(*args, **kwargs):
        print(args, kwargs)
        return func(*args, **kwargs)

    return wrap

@decorator
def boo(a, b, c):
    pass

try:
    boo(1, b=2, c=3, d=4)
except TypeError:
    traceback.print_exc(file=sys.stdout)
((1,), {'c': 3, 'b': 2, 'd': 4})
Traceback (most recent call last):
  File "<stdin>", line 19, in <module>
  File "<stdin>", line 10, in wrap
TypeError: boo() got an unexpected keyword argument 'd'

You guys feel weird to see me using the traceback.print_exc and sys.stdout. I use the Org-Mode to write this blog. It has so many useful features. I use the code block syntax which can be evaluated the codes to get their output. Due to it only collects output that came from stdout, I need to redirect it from stderr into stdout.

We can see the boo does not validate the parameters before the actual boo function being invoked. So we may need to validate the parameters before the func being invoked. And how to accessing the right parameter a of function boo in wrap function if we needs to do something with it.

To achieve that we need to inspect the parameter signatures of the wrapped function.

Use inspect.signature1 to do that.

import sys
import inspect
import traceback


def boo(a, b, c):
    pass

sig = inspect.signature(boo)

print(sig)

# accessing the specific parameter
bound_arguments = sig.bind(1, b=2, c=3)
print(bound_arguments)
print(bound_arguments.arguments['a'])

try:
    # validate the arguments
    sig.bind(1, b=2, c=3, d=4)
except TypeError:
    traceback.print_exc(file=sys.stdout)
(a, b, c)
<BoundArguments (a=1, b=2, c=3)>
1
Traceback (most recent call last):
  File "<stdin>", line 18, in <module>
  File "/usr/local/Cellar/python@3.9/3.9.1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 3062, in bind
    return self._bind(args, kwargs)
  File "/usr/local/Cellar/python@3.9/3.9.1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 3051, in _bind
    raise TypeError(
TypeError: got an unexpected keyword argument 'd'

We see the Signature.bind method can be used easily to validate and to access the specific parameter.

Now we combine them together.

import sys
import inspect
import functools
import traceback


def decorator(func):
    sig = inspect.signature(func)

    @functools.wraps(func)
    def wrap(*args, **kwargs):
        bound_arguments = sig.bind(*args, **kwargs)
        bound_arguments.apply_defaults()
        print(bound_arguments)
        return func(*args, **kwargs)

    return wrap


@decorator
def boo(a, b, c):
    pass

boo(1, b=2, c=3)

try:
    boo(1, b=2, c=3, d=4)
except TypeError:
    traceback.print_exc(file=sys.stdout)
<BoundArguments (a=1, b=2, c=3)>
Traceback (most recent call last):
  File "<stdin>", line 27, in <module>
  File "<stdin>", line 12, in wrap
  File "/usr/local/Cellar/python@3.9/3.9.1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 3062, in bind
    return self._bind(args, kwargs)
  File "/usr/local/Cellar/python@3.9/3.9.1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 3051, in _bind
    raise TypeError(
TypeError: got an unexpected keyword argument 'd'

This useful function inspect.signature1 came from PEP 362. It returns a inspect.Signature2 object which provides very useful methods to access the right parameters or to do some other things. See the PEP for more additional information.

This blog is written via Org-Mode, then being converted into Markdown, being released via Hugo at last.

Footnotes

1 The document link is https://docs.python.org/3.6/library/inspect.html?highlight=inspect#inspect.signature

2 The document link is https://docs.python.org/3.6/library/inspect.html?highlight=inspect#inspect.Signature




comments powered by Disqus