I'm writing some code to determine the name that an object is assigned to. This is for general debugging work and to further familiarize myself with python internals.
I have it structured as a class decorator so that all instances of that class will have their names recorded if it is possible to do. The code is fairly long so I won't post it unless asked. The general technique is as follows though
decorate the class' __init__
method with the code to do what I want
set caller = inspect.currentframe().f_back
and open inspect.getframeinfo(caller).filename
and send it to ast.parse
. I don't do any error checking here because (1) this is just for debugging/profiling/hacking (2) this exact process was 'just' completed or the code wouldn't be running. Is there a problem with this?
find the ast.Assignment
instance that causes the currently executing __init__
method to run
if len(assignment.targets) == 1
then there is only one item on the left hand side, and I can get the name out of targets[0].id
. In a simple form like a = Foo()
, then the assignment.value
is an instance of ast.Call
. if it's a literal (e.g. list), then value
will be that list and bail because the object I'm interested in isn't being assigned to a name.
What is the best way to confirm that assignment.value.func
is in fact type(obj).__call__
of the object that I'm interested in. I'm pretty sure that I'm guaranteed that It's "in there somewhere" or the code wouldn't even be running. I just need for it to be at the top level. The obvious thing to do is walk it and make sure that it doesn't contain any interior calls. Then I'm guaranteed that I have the name. (My reasoning is correct, I'm not sure if its assumptions are). This is not ideal though because if I'm interested in Foo
, this could lead me to toss away a = Foo(Bar())
because I don't know if it's a = Bar(Foo())
.
Of course I can just check assignment.value.func.id
but then somebody could have done Foobar = Foo
or something so I don't want to rely on this too heavily
Any help would be greatly appreciated. As always, I'm interested in any other suggestions or problems that I might be overlooking.
Also, I'm really surprised that I just had to invent the 'python-internals' tag.
The AST can't give you that answer. Try using frame.f_lasti and then peeking into the bytecode. If the next line isn't a STORE_FAST, you've got interior calls or something else
going on other than the simple assignment you're looking for.
def f():
f = sys._getframe()
i = f.f_lasti + 3 # capture current point of execution, advance to expected store
print dis.disco(f.f_code, i)
I don't know of how much help this is, but have you considered using the call to locals()
? It returns a dict
that contains the name and value of all the local variables.
For example:
s = ''
locals()
>>> {'__builtins__': <module '__builtin__' (built-in)>, '__package__': None, 's': '', '__name__': '__main__', '__doc__': None}
t = s # I think this is what is of most importance to you
locals()
>>> {'__builtins__': <module '__builtin__' (built-in)>, '__package__': None, 's': '', 't': '', '__name__': '__main__', '__doc__': None}
So you could traverse this dictionary and check which variables have (as their value) an object of the type that you are looking for.
Like I said, I don't know of how much help this answer is, but if you need clarification on anything, then do leave a comment and I will try to respond as best as I can.
I don't do any error checking here because (1) this is just for debugging/profiling/hacking (2) this exact process was 'just' completed or the code wouldn't be running. Is there a problem with this?
Yes:
Start a program
Wait unit it imports a particular module foo.py
Edit foo.py
Now code that's loaded in a Python process doesn't match code found on disk.
Yet another reason why disassembling the bytecode may be a better technique.
Here's how it's done. Much thanks to the anonymous clue giver. Much luck in your quest to rack up rep for your alt account.
import inspect
import opcode
def get_name(f):
"""Gets the name that the return value of a function is
assigned to.
This could be modified for classes as well. This is a
basic version for illustration that only prints out
the assignment instead of trying to do anything with it.
A more flexible way would be to pass it a callback for when
it identified an assignment.
It does nothing for assignment to attributes. The solution
for that isn't much more complicated though. If the
instruction after the function call is a a `LOAD_GLOBAL`,
`LOAD_FAST` or `LOAD_DEREF`, then it should be followed by
a chain of `LOAD_ATTR`'s. The last one is the attribute
assigned to.
"""
def inner(*args, **kwargs):
name = None
frame = inspect.currentframe().f_back
i = frame.f_lasti + 3
# get the name if it exists
code = frame.f_code
instr = ord(code.co_code[i])
arg = ord(code.co_code[i+1]) # no extended arg here.
if instr == opcode.opmap['STORE_FAST']:
name = code.co_varnames[arg]
elif instr in (opcode.opmap['STORE_GLOBAL'],
opcode.opmap['STORE_NAME']):
name = code.co_names[arg]
elif instr == opcode.opmap['STORE_DEREF']:
try:
name = code.co_cellvars[arg]
except IndexError:
name = code.co_freevars[arg - len(code.co_cellvars)]
ret = f(*args, **kwargs)
print opcode.opname[instr]
if name:
print "{0} = {1}".format(name, ret)
return ret
return inner
@get_name
def square(x):
return x**2
def test_local():
x = square(2)
def test_deref():
x = square(2)
def closure():
y = x
return closure
x = square(2)
test_local()
test_deref()()
It shouldn't be too hard to figure out assignments of the for list_[i] = foo()
either, including the value of i
by using frame.f_locals
. The tricky ones are going to be literals and when it's passed as an argument. Both of those cases should be pretty challenging.