Reputation: 185
Python obviously has a way to verify whether a function call has valid arguments (correct number of positional arguments, correct keyword arguments, etc). The following is a basic example of what I mean:
def test_func(x, y, z=None):
print(x, y, z)
test_func(2) # Raises a "missing positional argument" TypeError
test_func(1, 2, 3, a=5) # Raises an "unexpected keyword argument" TypeError
Is there a way that I can use this argument verification, without actually calling the function?
I'm basically trying to write a decorator that does some preprocessing steps based on the function arguments before calling the wrapped function itself, such as:
def preprocess(func):
def wrapper(*args, **kwargs):
# Verify *args and **kwargs are valid for the original function.
# I want the exact behavior of calling func() in the case of bad arguments,
# but without actually calling func() if the arguments are ok.
preprocess_stuff(*args, **kwargs)
func(*args, **kwargs)
return wrapper
I want my wrapper
function to verify that the arguments would be valid if used on the wrapped function before doing any preprocessing work.
I would like to take advantage of the checks Python already does every time you call a function and the various exceptions it will raise. I just do not want to actually call the function, because the function may not be idempotent. Writing my own checks and exceptions feels like reinventing the wheel.
Upvotes: 2
Views: 225
Reputation: 59118
You can't invoke the actual built-in argument verification for a function without calling the function, but you can use something pretty close.
The inspect
module has a function signature()
, which returns a Signature
object representing the argument signature of the function passed to it. That Signature
object has a bind()
method which attempts to create a BoundArguments
object using the arguments passed to it. If those arguments don't match the signature, a TypeError
is raised.
While this mostly behaves like the built-in argument binding logic, it has a few differences. For example, it can't always determine the signature of functions written in C, and its interaction with decorators will depend on whether they use functools.wraps
(or something else that sets the __wrapped__
attribute). That said, since the real argument binding logic is inaccessible, inspect.signature
is the best alternative.
We can use all this to create your decorator:
import functools
import inspect
def preprocess(func):
sig = inspect.signature(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
sig.bind(*args, **kwargs)
except TypeError:
pass # bad arguments; skip preprocessing
else:
print("Preprocessing: args=%r, kwargs=%r" % (args, kwargs))
# ... etc.
return func(*args, **kwargs)
return wrapper
Usage:
@preprocess
def test_func(x, y, z=None):
print(x, y, z)
>>> test_func(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 10, in wrapper
TypeError: test_func() missing 1 required positional argument: 'y'
>>> test_func(1, 2, 3, a=5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 10, in wrapper
TypeError: test_func() got an unexpected keyword argument 'a'
>>> test_func(1, 2)
Preprocessing: args=(1, 2), kwargs={}
1 2 None
Note that, if bad arguments are supplied, you do in fact want to call the function, because you "want the exact behavior of calling func() in the case of bad arguments" (to quote your comment), and the only way of getting the exact behaviour of calling an arbitrary function (even if that behaviour is to immediately fail) is to actually call it. What you don't want to do in such cases is the preprocessing, which the decorator above achieves for you.
Upvotes: 2