Search code examples
python-3.xintrospection

python3: getting defined functions from a code object?


In python3, I have the following code:

path = '/path/to/file/containing/python/code'
source = open(path, 'r').read()
codeobject = compile(source, path, 'exec')

I have examined codeobject, but I don't see any way to get a list of all the functions that are defined within that object.

I know I can search the source string for lines that begin with def, but I want to get this info from the code object, if at all possible.

What am I missing?


Solution

  • A code object is a nested structure; functions are created when the code object is executed, with their bodies embedded as separate code objects that are part of the constants:

    >>> example = '''\
    ... def foobar():
    ...     print('Hello world!')
    ... '''
    >>> codeobject = compile(example, '', 'exec')
    >>> codeobject
    <code object <module> at 0x11049ff60, file "", line 1>
    >>> codeobject.co_consts
    (<code object foobar at 0x11049fe40, file "", line 1>, 'foobar', None)
    >>> codeobject.co_consts[0]
    <code object foobar at 0x11049fe40, file "", line 1>
    >>> codeobject.co_consts[0].co_name
    'foobar'
    

    When you disassemble the top-level code object you can see that the function objects are created from such code objects:

    >>> import dis
    >>> dis.dis(codeobject)
      1           0 LOAD_CONST               0 (<code object foobar at 0x11049fe40, file "", line 1>)
                  2 LOAD_CONST               1 ('foobar')
                  4 MAKE_FUNCTION            0
                  6 STORE_NAME               0 (foobar)
                  8 LOAD_CONST               2 (None)
                 10 RETURN_VALUE
    

    The MAKE_FUNCTION opcode takes the code object from the stack, as well as the function name and any default argument values from the stack; you can see the LOAD_CONST opcodes preceding it that put the code object and name there.

    Not all code objects are functions however:

    >>> compile('[i for i in range(10)]', '', 'exec').co_consts
    (<code object <listcomp> at 0x1105cb030, file "", line 1>, '<listcomp>', 10, None)
    >>> compile('class Foo: pass', '', 'exec').co_consts
    (<code object Foo at 0x1105cb0c0, file "", line 1>, 'Foo', None)
    

    If you wanted to list what functions are loaded in the bytecode, your best bet is to use the disassembly, not look for code objects:

    import dis
    from itertools import islice
    
    # old itertools example to create a sliding window over a generator
    def window(seq, n=2):
        """Returns a sliding window (of width n) over data from the iterable
           s -> (s0,s1,...s[n-1]), (s1,s2,...,sn), ...
        """
        it = iter(seq)
        result = tuple(islice(it, n))
        if len(result) == n:
            yield result    
        for elem in it:
            result = result[1:] + (elem,)
            yield result
    
    def extract_functions(codeobject):
        codetype = type(codeobject)
        signature = ('LOAD_CONST', 'LOAD_CONST', 'MAKE_FUNCTION', 'STORE_NAME')
        for op1, op2, op3, op4 in window(dis.get_instructions(codeobject), 4):
            if (op1.opname, op2.opname, op3.opname, op4.opname) == signature:
                # Function loaded
                fname = op2.argval
                assert isinstance(op1.argval, codetype)
                yield fname, op1.argval
    

    This generates (name, codeobject) tuples for all functions that are loaded in a given code object.