Search code examples
pythonbytecodedisassembly

How can I see the bytecode of a decorated function?


I would like to see the bytecode of a decorated function with its decorator.

For example in the example below, fibonacci is decorated by memoized. However when I call 'dis.dis' on fibonacci, this will show me the byte code of the actual function.

I would like to be able to see if a function has been decorated and see the bytecode including the decoration part.

Am I totally misunderstanding some concept ?

import collections
import functools

class memoized(object):
   '''Decorator. Caches a function's return value each time it is called.
   If called later with the same arguments, the cached value is returned
   (not reevaluated).
   '''
   def __init__(self, func):
      self.func = func
      self.cache = {}

   def __call__(self, *args):
      if not isinstance(args, collections.Hashable):
         # uncacheable. a list, for instance.
         # better to not cache than blow up.
         return self.func(*args)
      if args in self.cache:
         print 'get cached version{}'.format(args)
         return self.cache[args]
      else:
         print 'compute {}'.format(args)
         value = self.func(*args)
         self.cache[args] = value
         return value

   def __repr__(self):
      '''Return the function's docstring.'''
      return self.func.__doc__

   def __get__(self, obj, objtype):
      '''Support instance methods.'''
      return functools.partial(self.__call__, obj)

@memoized
def fibonacci(n):
   "Return the nth fibonacci number."
   if n in (0, 1):
      return n
   return fibonacci(n-1) + fibonacci(n-2)

print fibonacci(12)

import dis
f = fibonacci
dis.dis(f)

Solution

  • You are calling dis.dis() on an instance; the memoized decorator is a class, and memoized(function) returns an instance of that class.

    For instances, all code or function objects in the values of the instance.__dict__ object are disassembled (because the dis() function assumes it is dealing with a class). Since the original function is a code object, it is disassembled. It is as if you called dis.dis(f.func); it is why the dis.dis() output starts with the line Disassembly of func.

    If you wanted to show the bytecode of the memoized.__call__ method, you'd have to either call dis.dis() on the memoized class (and see disassemblies for both __init__ and __call__), or disassemble the memoized.__call__ method directly, either by using dis.dis(memoized.__call__) or dis.dis(fibonacci.__call__) to give the disassembler a reference to the unbound or bound method.

    Since decorating is just syntactic sugar for calling another object with a function passed in, then replacing that function with the result, there is no such thing as a disassembly of the decorator with the original function together. The best you can do is disassemble the decorator callable and original function separately:

    >>> dis.dis(fibonacci.__call__)
     15           0 LOAD_GLOBAL              0 (isinstance)
                  3 LOAD_FAST                1 (args)
                  6 LOAD_GLOBAL              1 (collections)
                  9 LOAD_ATTR                2 (Hashable)
                 12 CALL_FUNCTION            2
                 15 POP_JUMP_IF_TRUE        31
    
     18          18 LOAD_FAST                0 (self)
                 21 LOAD_ATTR                3 (func)
                 24 LOAD_FAST                1 (args)
                 27 CALL_FUNCTION_VAR        0
                 30 RETURN_VALUE
    
     19     >>   31 LOAD_FAST                1 (args)
                 34 LOAD_FAST                0 (self)
                 37 LOAD_ATTR                4 (cache)
                 40 COMPARE_OP               6 (in)
                 43 POP_JUMP_IF_FALSE       71
    
     20          46 LOAD_CONST               1 ('get cached version{}')
                 49 LOAD_ATTR                5 (format)
                 52 LOAD_FAST                1 (args)
                 55 CALL_FUNCTION            1
                 58 PRINT_ITEM
                 59 PRINT_NEWLINE
    
     21          60 LOAD_FAST                0 (self)
                 63 LOAD_ATTR                4 (cache)
                 66 LOAD_FAST                1 (args)
                 69 BINARY_SUBSCR
                 70 RETURN_VALUE
    
     23     >>   71 LOAD_CONST               2 ('compute {}')
                 74 LOAD_ATTR                5 (format)
                 77 LOAD_FAST                1 (args)
                 80 CALL_FUNCTION            1
                 83 PRINT_ITEM
                 84 PRINT_NEWLINE
    
     24          85 LOAD_FAST                0 (self)
                 88 LOAD_ATTR                3 (func)
                 91 LOAD_FAST                1 (args)
                 94 CALL_FUNCTION_VAR        0
                 97 STORE_FAST               2 (value)
    
     25         100 LOAD_FAST                2 (value)
                103 LOAD_FAST                0 (self)
                106 LOAD_ATTR                4 (cache)
                109 LOAD_FAST                1 (args)
                112 STORE_SUBSCR
    
     26         113 LOAD_FAST                2 (value)
                116 RETURN_VALUE
                117 LOAD_CONST               0 (None)
                120 RETURN_VALUE
    >>> dis.dis(fibonacci.func)
     39           0 LOAD_FAST                0 (n)
                  3 LOAD_CONST               4 ((0, 1))
                  6 COMPARE_OP               6 (in)
                  9 POP_JUMP_IF_FALSE       16
    
     40          12 LOAD_FAST                0 (n)
                 15 RETURN_VALUE
    
     41     >>   16 LOAD_GLOBAL              0 (fibonacci)
                 19 LOAD_FAST                0 (n)
                 22 LOAD_CONST               2 (1)
                 25 BINARY_SUBTRACT
                 26 CALL_FUNCTION            1
                 29 LOAD_GLOBAL              0 (fibonacci)
                 32 LOAD_FAST                0 (n)
                 35 LOAD_CONST               3 (2)
                 38 BINARY_SUBTRACT
                 39 CALL_FUNCTION            1
                 42 BINARY_ADD
                 43 RETURN_VALUE
    

    You can see from the fibonacci.__call__ disassembly it'll call self.func() (byte codes 18 through 27), which is why you then would look at fibonacci.func.

    For function decorators using a closure, you'd have to reach into the wrapper closure to extract the original function, by looking at the __closure__ object:

    >>> def memoized(func):
    ...    cache = {}
    ...    def wrapper(*args):
    ...       if not isinstance(args, collections.Hashable):
    ...          # uncacheable. a list, for instance.
    ...          # better to not cache than blow up.
    ...          return func(*args)
    ...       if args in cache:
    ...          print 'get cached version{}'.format(args)
    ...          return cache[args]
    ...       else:
    ...          print 'compute {}'.format(args)
    ...          value = func(*args)
    ...          cache[args] = value
    ...          return value
    ...    return wrapper
    ...
    >>> @memoized
    ... def fibonacci(n):
    ...    "Return the nth fibonacci number."
    ...    if n in (0, 1):
    ...       return n
    ...    return fibonacci(n-1) + fibonacci(n-2)
    ...
    >>> fibonacci.__closure__
    (<cell at 0x1035ed590: dict object at 0x103606d70>, <cell at 0x1036002f0: function object at 0x1035fe9b0>)
    >>> fibonacci.__closure__[1].cell_contents
    <function fibonacci at 0x1035fe9b0>