The first line of the code in the function, 'func', below is a print statement. It prints/generates the following sort of cross between a tuple and a dictionary, (to the right of the first equals sign) containing all func’s parameters along with the default values for those parameters:
str(inspect.signature(func)) = (a='my', b='dog', c='chews', d='bones')
I want to modify the print statement on the first line of 'func' (as distinguished from print Statement #2 far below):
print('str(inspect.signature(X) = ' + str(inspect.signature(X))
Here is the function, 'func' (containing a print statement on the first line of 'func') and, following the definition of 'func', a second print statement (on the last line of the code) supplying non-default values to 'func':
def func(a='my', b='dog', c='chews', d='bones'):
print('str(inspect.signature(func)) = ' + str(inspect.signature(func)))
return a + ' ' + b + ' ' + c + ' ' + d
print('Statement #2: ' + func(a='her', b='cat', c='drinks', d='milk'))
Any suggestions would be much appreciated, preferably with a link or some explanation.
In a normal context, you should absolutely do this with a decorator, because what you're trying to do is a really bad idea and requires messing with the grittier parts of Python, whilst also being the pinnacle of spaghetti code.
Here's a decorator that does the above:
import inspect
def wrap(f):
def inner(*args, **kwargs):
print(f'str(inspect.getsignature({f.__name__})) =', inspect.signature(f)) # use __name__ to get its' name
return f(*args, **kwargs)
return inner
@wrap
def func(a='my', b='dog', c='chews', d='bones'):
return a + ' ' + b + ' ' + c + ' ' + d
However, that's no fun!
Python uses frames to implement the call stack, and provides a convenient library for fiddling with them, which you've already come across - the inspect
library.
inspect.currentframe
returns a FrameInfo
object, which will give us the frame in which that was called - in this case, the frame representing our function.
We then rip out the f_code
attribute of that frame, which is a code object - the meat behind every function. However, a code object isn't a function yet, and you asked for one. Irritatingly, there's no way to pull the default arguments out of a frame.
We get around this by just jumping one up the call stack, and using the name we get from f_code.co_name
to access the f_locals
of the caller. This gets us our function.
Here's an implementation:
import inspect
def func(a='my', b='dog', c='chews', d='bones'):
frame = inspect.currentframe()
name = frame.f_code.co_name
previous_frame_locals = frame.f_back.f_locals
f = previous_frame_locals[name]
print(f'str(inspect.getsignature({name})) =', inspect.signature(func))
return a + ' ' + b + ' ' + c + ' ' + d
That's pretty long-winded though, and you wanted an expression.
import inspect
def func(a='my', b='dog', c='chews', d='bones'):
print(f'str(inspect.getsignature({(name := (frame := inspect.currentframe()).f_code.co_name)})) =', inspect.signature(frame.f_back.f_locals[name]))
return a + ' ' + b + ' ' + c + ' ' + d
Problem is, that's completely unreadable. We could make a helper function!
import inspect
def helper():
frame = inspect.currentframe().f_back # make an additional step back up the stack
name = frame.f_code.co_name
previous_frame_locals = frame.f_back.f_locals
f = previous_frame_locals[name]
print(f'str(inspect.getsignature({name})) =', inspect.signature(func))
def func(a='my', b='dog', c='chews', d='bones'):
helper()
return a + ' ' + b + ' ' + c + ' ' + d
That's probably the best solution, even if it's still a total crime against Python. In code, just use the decorator above.
As a final note, you don't actually need inspect
to get the current frame - you can do shenanigans with raising an error and using the traceback to get the frame. If we were doing other manipulations than inspect.signature
, it would save us an import, which would be nice. It's also unreadable, so don't do this. Here it is anyway, for educational purposes.
import inspect
def helper():
# get the frame
try:
raise # anything that raises an exception, like 1/0, will work.
except Exception as e:
traceback = e.__traceback__
helper_frame = traceback.tb_frame
frame = helper_frame.f_back # make an additional step back up the stack
name = frame.f_code.co_name
previous_frame_locals = frame.f_back.f_locals
f = previous_frame_locals[name]
print(f'str(inspect.getsignature({name})) =', inspect.signature(func))
Be careful that you clear your frames, also - if you don't, it can create reference cycles, and memory leaks are bad juju.
All of this is CPython specific, and will break when the API is updated, which is regularly.
Just use the decorator.