Suppose that we have some function:
def funky_the_function(pos_zero, pos_one, *, default=None, sep = " "):
pass
Can you write a function-decorator which prints the function call?
That is, we want to print the function name along with the to-stringed arguments substituted into the function parameters.
@print_calling_args
def funky_the_function(pos_zero, pos_one, *, default=None, sep = " "):
pass
funky_the_function(1, 2, sep = "|-|")
# prints: `funky_the_function(1, 2, sep = "|-|")`
Some Requirements:
- the names of key-word arguments must be shown provided that the key-word argument is not assigned a default value. For example, we might print
kwarg = 99
- the names of positional arguments must be NOT shown. we print
1, 2
nota = 1, b = 2
- default values must not be printed.
- Each stringed argument must be at most 10 characters long. The 10 character limit does not include the keyword if the argument is a keyword argument
- Each stringed argument must not contain line-breaks
- We must have a function name for functors (class objects with a
__call__
method)
Approach One
def print_calling_args(f):
assert(callable(f))
@functools.wraps(f)
def wrapper(*args, **kwargs):
sargs = ", ".join(str(arg) for arg in args)
skwargs = ", ".join(kw + " = " + str(arg) for kw, arg in kwargs.items())
all_sargs = ", ".join((sargs, skwargs))
print(f.__qualname__, "(", all_sargs, ")", sep = "")
return f(*args, **kwargs)
return wrapper
There are some issues with Attempt One. I will explain thee issues later.
Approach Two (Using inspect.getcallargs
)
import inspect
def print_calling_args(f):
assert(callable(f))
@functools.wraps(f)
def wrapper(*args, **kwargs):
cargs = inspect.getcallargs(f, *args, **kwargs)
print(cargs)
all_sargs = ", ".join(kw + " = " + str(arg) for kw, arg in cargs.items())
print(f.__qualname__, "(", all_sargs, ")", sep = "")
return f(*args, **kwargs)
return wrapper
Approach Three (Using inspect.signature
)
import functools
import inspect
def print_calling_args(f):
assert(callable(f))
@functools.wraps(f)
def wrapper(*args, **kwargs):
siggy = inspect.signature(f)
bound_siggy = siggy.bind(*args, **kwargs)
all_sargs = ", ".join(kw + " = " + str(arg) for kw, arg in bound_siggy.arguments.items())
print(f.__qualname__, "(", all_sargs, ")", sep = "")
return f(*args, **kwargs)
return wrapper
Mistakes with my Attempts
Mistake: Printing Line Breaks and Overly Long Strings
If you convert an argument to string by using
str(arg)
then the resulting string can contain new-line characters\n
or carriage returns\r
. For example, suppose you pass a pandas dataframe into a function?
- I require that the stringed arguments all fit on one line (no line feeds)
- I require that string arguments be shorter than one mile long (we must truncate the string)
We can truncate the strings as follows:
def truncate(stryng:str, nchars:int = 10):
# BEGIN ERROR CHECKING
stryng = "".join(str(ch) for ch in stryng)
nchars = int(str(nchars))
# END ERROR CHECKING
snip = " [...]"
if nchars > len(stryng):
return stryng
return (stryng[:nchars - len(snip)] + snip)[:min(len(stryng), nchars)]
We can also get rid of the line-feeds (\n
), etc....
def sani(whole:str) ->str:
"""
gets rid of all of the line-feed characters,
carriage-return characters, and other nasty characters in a string
INPUT:
A passage of English text containing new line character
"hello\nworld"
"hello" + chr(10) + "world"
OUTPUT:
string containing back-slash followed by a letter "n"
"".join(["hello", chr(92), chr(110), "world"])
"""
result = "\\".join(repr(part)[1:-1] for part in whole.split("\\"))
return result
Mistake: Cannot Get the Name of a Functor
Another issue with my attempts is that Functors (also known as "function objects") do not have an attribute named
__qualname__
. That is, if you define a class which has a__call__
method, then we cannot easily print the function name to string. I require that we print something intelligible for functors; maybe the name of the class they are instantiated from followed by()
.
funky = Functor()
funky = decorate(funky)
r = funky(1, 2, 3, kwargy = int("python", 36))
# prints:
# Functor()(1, 2, 3 kwargy = 1570137287)
Here is a functor:
# EXAMPLE OF A `FUNCTOR`
class Multiplier:
def __init__(self, m:int):
self._m = int(str(m))
def __call__(self, num:float)
return num * self._m;
multi_the_multiplier = Multiplier(10**6)
# Decorate `multi_the_multiplier` with ` print_calling_args`
multi_the_multiplier = print_calling_args(multi_the_multiplier)
# ERROR: `multi_the_multiplier` does not have a `__qualname__`
r = multi_the_multiplier(1)
r = multi_the_multiplier(2)
r = multi_the_multiplier(3)