It's not uncommon to have a function that acts as an adapter, wrapping another existing function, having its same parameters and performing some action before and/or after invoking the original function with those parameters. JavaScript provides a very convenient way to invoke the original function without having to write again that full list of arguments in the call, just using the arguments array-like object and the spread operator. I mean:
function doAction(a, b, c) {
}
function checkParams(a, b, c) {
if (a === "undefined") {
return null;
}
return doAction(...arguments);
}
In many cases the arguments object is not necessary, we can just use the rest operator in the adapter function rather than the list of arguments. That's great when the adapter function is not doing anything with the arguments, for example when creating generic wrappers like this:
function doAction(a, b, c) {
}
function createLoggingWrapper(fn) {
return (...params) => {
console.log("started");
res = fn(...params);
console.log("finished");
return res;
};
}
In Python the equivalent to the rest-spread operaters is the packing-unpacking operator (that given that Python features named parameters has extra support for that) . So we can write the second example just like this:
from functools import wraps
def doAction(a, b, c):
print(f"{a}{b}{c}")
def createLoggingWrapper(fn):
@wraps(fn)
def logged(*args, **kwargs):
print("started")
res = fn(*args, **kwargs)
print("finished")
return res
return logged
doAction = createLoggingWrapper(doAction)
doAction("AA", c="CC", b="BB")
But what about the first example? We don't have a direct equivalent to the arguments object, but we have a workaround that is almost equivalent, using locals(). I already talked about the locals() function in a previous post. It returns the list of variables in the "local namespace" at that moment (that is function arguments and local variables), so if we run it before any local variable is declared we can use it as the JavaScript arguments object. Let's see:
def format(name, age, city, country):
return f"formatting: {name} - {age} [{city}, {country}]"
def format_with_validation(name, age, city, country):
if city:
print("validation OK")
return format(**locals()) # equivalent to: format(name=name, age=age, city=city, country=country)
else:
print("validation KO")
return None
print(format_with_validation("Francois", 40, "Paris", "France"))
print(format_with_validation("Francois", 40, "Lyon", "France"))
# validation OK
# formatting: Francois - 40 [Paris, France]
# validation OK
# formatting: Francois - 40 [Lyon, France]
There's one gotcha. locals() returns also those variables (free-vars) trapped by closures. So if our wrapping function happened to be working as a closure, this technique will not work, as we'll be passing extra parameters to the wrapped function (the "counter" variable in the example below).
def format(name, age, city, country):
return f"formatting: {name} - {age} [{city}, {country}]"
def create_format_with_validation():
counter = 0
def format_with_validation(name, age, city, country):
nonlocal counter
counter +=1
print(f"invokation number: {counter}")
if city:
print("validation OK")
return format(**locals()) # equivalent to: format(name=name, age=age, city=city, country=country)
else:
print("validation KO")
return None
return format_with_validation
format_with_validation = create_format_with_validation()
try:
print(format_with_validation("Francois", 40, "Paris", "France"))
except Exception as ex:
print(f"Error: {ex}")
# TypeError: format() got an unexpected keyword argument 'counter'. Did you mean 'country'?
Well, we can easily fix that by removing from the object returned by locals the entries corresponding to trapped variables. Indeed, Python is so amazingly instrospective that we can automate that. The code object of one function has a co_freevars attribute that gives us the names of the freevars used by that function. So we can write a "remove_free_vars" function and do this:
def remove_free_vars(loc_ns: dict, fn) -> dict:
return {
key: value for key, value in loc_ns.items()
if key not in fn.__code__.co_freevars
}
def create_format_with_validation_closure_aware():
counter = 0
def format_with_validation(name, age, city, country):
nonlocal counter
counter +=1
print(f"invokation number: {counter}")
if city:
print("validation OK")
return format(**(remove_free_vars(locals(), format_with_validation)))
else:
print("validation KO")
return None
return format_with_validation
format_with_validation = create_format_with_validation_closure_aware()
print(format_with_validation("Francois", 40, "Paris", "France"))
There's one additional gotcha. As locals() returns a dictionary-like object, if the function to be invoked has been defined restricting that some of its arguments can not be provided as named arguments (that odd feature that I described here).