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)
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>