pythonattributesinspect

From inside a python function, use a var representing the func object, to get the func's parameters and the values supplied


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):

  1. So that each time 'func' is called, the print statement prints the names of the parameters along with: [i] the supplied values where they are different from the parameters’ default values, and [ii] the default values if no other values are supplied; and
  2. By replacing the string 'func' with a code variable that represents the function object, where the function object is the function in which the code/variable is located (in this case, 'func'), as an object. To illustrate, assume X = PSEUDO_CODE_REPRESENTING_THIS_FUNCTION_OBJECT, and that the following line is the first line of code in the function:
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.


Solution

  • What you should do

    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!

    What you shouldn't do

    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.