Unfortunately func_code.co_names
is not likely going to help much. This contains all names that are accessed within the code segment, including global variables, in order of appearance.
class Test(object):
def calc_a(self):
return self.b + self.c
def calc_x(self):
return self.y.a + self.y.b
>>> Test.calc_a.func_code.co_names
('b', 'c')
>>> Test.calc_x.func_code.co_names
('y', 'a', 'b')
It is not possible to tell from this array if 'a' and 'b' loaded from 'self' or from 'self.y'. Generally, the only way to know the access pattern of a bit of code without executing it is to disassemble it.
>>> import dis
>>> dis.dis(Test.calc_x)
23 0 LOAD_FAST 0 (self)
3 LOAD_ATTR 0 (y)
6 LOAD_ATTR 1 (a)
9 LOAD_FAST 0 (self)
12 LOAD_ATTR 0 (y)
15 LOAD_ATTR 2 (b)
18 BINARY_ADD
19 RETURN_VALUE
We see that the function loads the 'self' variable (which is always co_varnames[0]
for a bound function), then from that object loads the attribute 'y' (co_names[0]
), and then from that object loads the attribute 'a' (co_names[1]
). A second stack object is pushed from self.y.b, then the two are added.
Look at the source of dis.py in the standard lib to see how the C Python binary code is disassembled. Loads of the 0th variable will be important for bound functions. Another handy point is that the arguments to the function are co_varnames[:co_argcount]
(the rest or varnames are locals) and co_freevars
are variables from an enclosing non-global scope.