19

If I have a reference to a function I can check it's code object f.__code__, get a signature, then perform later checks against this signature to see if the code changed. This is good. But what if one of the funciton's dependencies changed? E.g.

def foo(a, b):
    return bar(a, b)

Let's say foo remained the same, but bar changed. Is there a way I can check foo's dependencies 'live' via the foo.__code__ object (vs. parsing the text and using AST)?

Cong Ma
  • 10,692
  • 3
  • 31
  • 47
Maxim Khesin
  • 597
  • 4
  • 10
  • 10
    May I ask why you are doing this? What is the broader purpose? – John Kugelman Aug 08 '13 at 19:08
  • 6
    If you need to do this, something has gone horribly wrong. (Also, holy crap. Python lets you reassign a function's code. That's getting filed under "features to never, ever touch".) – user2357112 Aug 08 '13 at 19:11
  • 5
    Regardless of the intention it seems like an interesting, if slightly academic, question still. – Flexo Aug 08 '13 at 19:14
  • 2
    Not academic :). I have a pipeline object that accepts a sequence of generally simple 'processors' each one accepts a record originally coming from a static source, augments it and passes on to the next processor. This is a good way to express needed preprocessing, but is not efficient. So the cache object dumps the processed records at the end to a (json) file to serve as a cache. The reason to get signatures of the functions is to force a cache refresh when something changes. – Maxim Khesin Aug 08 '13 at 20:32
  • @MaximKhesin I was planning to do the exact same thing, wow. My setup was for ML / Data processing pipelines and only recompute values when the function code changes. – Abhishek Divekar Nov 03 '19 at 06:49

2 Answers2

5

I'm just poking around at the __code__ object here, so I can't say that this would work for sure, but it looks to me like you could use the co_names tuple to (recursively) traverse the call graph rooted at a particular function, in order to build up some sort of hash of the transitive closure of the functions that could possible be called. (I don't think it'd be possible to include only the functions that will be called for a particular input, but you could be conservative and include every possible call.)

To do this you'd need to maintain some sort of symbol table to be able to look up the names of the functions that get called. But once you start going down this path, it seems like you're basically going to build up your own equivalent of the AST. So, why not just use the AST to start with ?

lmjohns3
  • 7,422
  • 5
  • 36
  • 56
5

You may to compare bytecode attributes on code object using method.__code__.co_code. For example lets define two classes:

>>> class A:
...     a = 1
...     def b(self, b):
...             print(self.a + b)
... 
>>> class B:
...     a = 1
...     def b(self, b):
...             print(self.a + b)
... 
>>> A().b.__code__.co_code
'|\x00\x00j\x00\x00|\x01\x00\x17GHd\x00\x00S'
>>> B().b.__code__.co_code
'|\x00\x00j\x00\x00|\x01\x00\x17GHd\x00\x00S'
>>> A().b.__code__.co_code == B().b.__code__.co_code
True

and if method b in class A is changed:

>>> class A:
...     a = 1
...     def b(self, b):
...             print(b + self.a)
... 
>>> A().b.__code__.co_code
'|\x01\x00|\x00\x00j\x00\x00\x17GHd\x00\x00S'
>>> A().b.__code__.co_code == B().b.__code__.co_code
False

or use inspect method inspect.getsource(object) that:

Return the text of the source code for an object. The argument may be a module, class, method, function, traceback, frame, or code object. The source code is returned as a single string.

And if you want to know whether the code has changed in dynamic you may need to reload your class with importlib and compare bytecode.

Eugene Lopatkin
  • 2,351
  • 1
  • 22
  • 34
  • Note that this won't detect functions that are different by virtue of closing over different variables... at least if memory serves me correctly. – Att Righ Feb 25 '22 at 11:04