It might not be advisable, but this can be done - at least to some extent - by parsing the bytecode of the caller. Specifically, you would want to get the stack frame of the caller with eg. frame = inspect.stack()[-2][0]
, and look at frame.f_code.co_code
for the raw bytecode and frame.f_lasti
for the index of the last instruction executed in that frame - which will be the CALL_FUNCTION
opcode which caused your function to be called. (As long as you haven't returned yet - after that f_lasti
will get updated as the execution proceeds in the callers frame)
Now, parsing the bytecode isn't really hard at all, at least until you get to reconstructing control flow (which you can avoid if you can make the assumption the called doesn't use eg. the ternary operator, and
or or
in the arguments to the call to your function) - but while it isn't hard per se, if you haven't done anything like it before (ie. playing with the internals of things, or other marginally "low-level" stuff), it might be a tall mountain to climb, depending on the person; it likely won't be a quick exercise.
So, what about the complications? Well, for one, bytecode is an implementation detail of CPython, so this won't work in alternative interpreters like, for example, Jython. In addition, the bytecode can change from CPython version to the next, so most CPython versions will need slightly different parsing code.
Earlier, I also said "to some extent" - what did I mean by that? Well, I already mentioned handling conditionals might be hard. In addition, you can't get the variable name, if the value wasn't stored in a variable! For example, your function might be called like f(1, 2, 3)
, f(mylist[0], mydict['asd'], myfunc())
or f(*make_args(), **make_kwargs())
- in the first two cases you might just decide that instead of a variable name, what you really want to know is the expression that corresponds to each argument... but what about the last case? Either expression might correspond to more than one argument, and you don't know which argument came from which expression! And in general, you can't know that either - since the values came from function calls, you can't look at them without calling the functions again - and nothing guarantees the functions won't have side-effects or that they'll return the same values again. Or maybe, like earlier with the conditionals, you might be able to assume the caller doesn't do any of that.
In short, it is possible - to some degree - and it might not even be especially hard to do - at least if you know what you're doing - but it's certainly not going to be simple, fast, general nor advisable.
Edit: If, for some reason - after all that - you still feel like you'd like to do it for some reason (or if you're just curious what it'd look like), here's an extremely limited proof-of-concept to get you started. (Only works for CPython3.4, only positional arguments, only local variables. Also, for <3.4, there is no get_instructions()
, so the parsing has to be done manually. Also, it gets more and more complex as you add support for more opcodes - which might depend on the results of previous opcodes)
import inspect, dis
def get_caller_arg_names():
"""Get the argument names used by the caller of our caller"""
frame = inspect.stack()[-2][0]
lasti, code = frame.f_lasti, frame.f_code
insns = list(dis.get_instructions(code))
for call_ind, insn in enumerate(insns):
if insn.offset == lasti:
break
else:
assert False, "Frame's lasti doesn't match the offset of any of its instructions!"
insn = insns[call_ind]
assert insn.opcode == dis.opmap['CALL_FUNCTION'], "Frame's lasti doesn't point to a CALL_FUNCTION instruction!"
assert not insn.arg & 0xff00, "This PoC doesn't support keyword arguments!"
argcount = insn.arg & 0xff
assert call_ind >= argcount, "Bytecode doesn't have enough room for loading all the arguments! (At least without magic)"
argnames = []
for insn in insns[call_ind-argcount:call_ind]:
assert insn.opcode == dis.opmap['LOAD_FAST'], "This PoC only supports direct local variables (LOAD_FAST) without any hijinks!"
argnames.append(insn.argval)
return argnames
if __name__ == '__main__':
def callee(arg1, arg2, arg3):
print(get_caller_arg_names())
def caller():
a, b, c = 1, 2, 3
callee(a, b, c)
caller()