6

While trying to figure out if a function is called with the @decorator syntax, we realized that inspect has a different behaviour when looking at a decorated class that inherits from a superclass.

The following behaviour was found with CPython 3.6.2 under Windows 10.

It was also reproduced in CPython 3.7.0 under Linux 64 bits.

import inspect

def decorate(f):
    lines = inspect.stack()[1].code_context
    print(f.__name__, lines)

    return f

@decorate
class Foo:
    pass

@decorate
class Bar(dict):
    pass

Output

Foo ['@decorate\n']
Bar ['class Bar(dict):\n']

Why does inheritance change the behaviour of inspect?

Olivier Melançon
  • 19,112
  • 3
  • 34
  • 61
  • Can't reproduce that behavior on Python 3.6. Are you sure you're not using Python 2.x? – kindall Sep 05 '18 at 20:31
  • I doubt it's relevant here, but just in case, it would probably be better to make `Bar` inherit from a Python type rather than a builtin (and make the decorator `return f`, so you can use `Foo` as that class to inherit). – abarnert Sep 05 '18 at 20:33
  • 2
    *Can* reproduce on Python 3.6 here, which kind of surprises me, given the results of some other experiments. – user2357112 supports Monica Sep 05 '18 at 20:34
  • @kindall Python 3.6 under Windows 10 – Olivier Melançon Sep 05 '18 at 20:34
  • @abarnert It is unrelated indeed. Although, I'll update for everyone's peace of mind – Olivier Melançon Sep 05 '18 at 20:35
  • 2
    I can repro on 3.6 in the interactive interpreter, both stock and IPython, on Mac, Linux, and repl.it. But when I put the same code in a script, the code_context is `None` for both functions on Linux and repl.it. – abarnert Sep 05 '18 at 20:39
  • Anyway, I'm pretty sure `code_context` is just generated from `filename` and `lineno` via `linecache`, so the question is: is the `lineno` different by one, or is `linecache` acting weird? – abarnert Sep 05 '18 at 20:40
  • 2
    I can repro in a script, and I wouldn't expect this to work interactively unless you're on something like IPython, since interactive mode shouldn't save the source code. – user2357112 supports Monica Sep 05 '18 at 20:40
  • 2
    It's `lineno` differing by 1, [as you can see on repl.it](https://repl.it/repls/MajorArtisticCharacterencoding), which narrows down what to look at. – abarnert Sep 05 '18 at 20:42
  • 1
    Simpler test: the frame objects have different `f_lineno` values, [again at repl.it](https://repl.it/repls/MajorArtisticCharacterencoding). So it's nothing to do with `inspect`, it's how CPython tracks line numbers in frame objects… – abarnert Sep 05 '18 at 20:45
  • 1
    Also, [an empty base list is the same as not having a base list, but `object` is the same as having one](https://repl.it/repls/MajorArtisticCharacterencoding). – abarnert Sep 05 '18 at 20:47
  • 5
    Disassembly with `dis` shows that the line number is advanced for the LOAD_NAME that loads the base class object, which doesn't happen with no base class. – user2357112 supports Monica Sep 05 '18 at 20:49
  • @abarnert You've posted the same link 3 times now :D – Aran-Fey Sep 05 '18 at 20:49
  • @Aran-Fey Oops; it I guess I disabled versioning… – abarnert Sep 05 '18 at 20:50
  • @user2357112 I see the same, I think you got it. – Olivier Melançon Sep 05 '18 at 20:51
  • @user2357112 Yeah, that's it. You could test further—what if you do a `LOAD_FAST` or `LOAD_CONST` for the base—if you really want to. – abarnert Sep 05 '18 at 20:53
  • 1
    (What confuses me now is why pdb pointed to `@dec` for both cases when I put a `pdb.set_trace()` in the decorator.) – user2357112 supports Monica Sep 05 '18 at 20:53

1 Answers1

6

Further experiment shows that this is a quirk of Python's line number assignment. Particularly, if we use dis to see the disassembly of code with and without a base class:

import dis
import sys

dis.dis(sys._getframe().f_code)

def dec(): pass

@dec
class Foo: pass

@dec
class Bar(Foo): pass

We see that for Foo, the instructions involved have line number 8 (corresponding to the @dec line):

  8          58 LOAD_NAME                4 (dec)
             61 LOAD_BUILD_CLASS
             62 LOAD_CONST               4 (<code object Foo at 0x2b2a65422810, file "./prog.py", line 8>)
             65 LOAD_CONST               5 ('Foo')
             68 MAKE_FUNCTION            0
             71 LOAD_CONST               5 ('Foo')
             74 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             77 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             80 STORE_NAME               5 (Foo)

But for Bar, the line number advances from 11 to 12 for the LOAD_NAME that loads the base class:

 11          83 LOAD_NAME                4 (dec)
             86 LOAD_BUILD_CLASS
             87 LOAD_CONST               6 (<code object Bar at 0x2b2a654a0f60, file "./prog.py", line 11>)
             90 LOAD_CONST               7 ('Bar')
             93 MAKE_FUNCTION            0
             96 LOAD_CONST               7 ('Bar')

 12          99 LOAD_NAME                5 (Foo)
            102 CALL_FUNCTION            3 (3 positional, 0 keyword pair)
            105 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
            108 STORE_NAME               6 (Bar)

With no base class, the parent frame's f_lineno is on the @ line when the decorator runs. With a base class, the parent frame is on the load-the-base-class line.

user2357112 supports Monica
  • 215,440
  • 22
  • 321
  • 400