5

How do i detect or capture change of value of a global variable in python

variable = 10
print(variable)
variable = 20 
# Detect changes to the value using a signal to trigger a function

UPDATE AST docs - GOOD INTRO https://greentreesnakes.readthedocs.io/en/latest/

Gary
  • 2,293
  • 2
  • 25
  • 47
  • 1
    I don't know if python can do this but `tkinter` use classes for this - ie. `IntVar()` uses `get()/set()` to work with data but it has also `tracer(callback)` to assign function(s) which will be executed when value is changed. Similar classes - `Properties` - uses `Kivy`. So you could create own classes with similar functionality. – furas Jan 10 '20 at 03:58
  • 1
    Is this in a standalone script, or in a larger application? Because you can use async processes within a loop to send signals/flags to be captured on the next loop – Sparrow1029 Jan 10 '20 at 04:15
  • Standalone script and I am looking to capture only variable value changes @Sparrow1029 – Gary Jan 10 '20 at 04:40
  • 1
    What if you wrap it all in a while loop and at the top of each loop detect if any have changed? Sorry I don't know more about the script you're trying to write, but generally you need to have a separate watcher/listener process of some kind in order to detect changes in variables, which usually ends up more complicated than a single script... – Sparrow1029 Jan 10 '20 at 04:47
  • Is it important to capture the variable change *immediately* or do you just need to know within a reasonably short amount of time? I believe in CPython, the global variables are stored in a `dict`, a builtin type implemented in C and so you cannot override the `__setitem__`. In that case you may have to use some kind of monitor process – Hymns For Disco Jan 10 '20 at 04:59
  • 1
    Would it be acceptable if you can only capture changes applied from other modules? e.g. in module `a` you have a global `variable`, and then in module `b`, `import a; a.variable = 123`. This specific case is more doable as you can override the module object that `import a` results in, and then override the `__setitem__` of that object. At that point though you are really going out of your way to do something that maybe is not meant to be done – Hymns For Disco Jan 10 '20 at 05:04
  • @HymnsForDisco @ Sparrow1029 AH, no. I wanted the variable change in the same module. It is just one file. Second, I want to capture the variable change detection as it happens. So probably like a event listener for value change. What I tried was a observer pattern on the type of variable and seems it is not really capable of capturing the value change. What I am trying now is changing the ast.Str but unsure how to override the implementation. Any way I can add the observer to ast.Str (for ex)? Any refs on how to change the ast.Str or builtin str to automatically add the fn to object class? – Gary Jan 10 '20 at 08:06

2 Answers2

3

To my knowledge, it is not possible to generically capture the assignment of a global symbol in Python (At least in CPython where globals are stored in a dict in the module object, both are C types that cannot be monkey patched).

Here's a simple workaround that's a bit of a compromise. Use a wrapper object to store your monitored variables, and define __setattr__ to do whatever you want to do before (or after) setting an attribute.

class CaptureOnSetAttribute:
    def __setattr__(self, attr, value):
        # our hook to do something
        print(f'set value of {attr} to {value}')
        # actually set the attribute the normal way after
        super().__setattr__(attr, value)


wrapper_object = CaptureOnSetAttribute()

The compromise of course is that now instead of writing something like:

monitored_global = value

You must now write:

wrapper_object.monitored_attribute = value
funnydman
  • 9,083
  • 4
  • 40
  • 55
Hymns For Disco
  • 7,530
  • 2
  • 17
  • 33
  • Ok. Any tips on how to modify the ast and object type class def? Second, I am struggling to find the class implementation of the str. Is it documented somewhere? I cant see the definition of object functions. – Gary Jan 10 '20 at 09:31
  • 1
    @Gary what do you mean by "ast" and modifying it? If you are using CPython, then then `object`, `type`, and `str` are all implemented in C, I think most of the relevant code is here https://github.com/python/cpython/tree/master/Objects. If you're talking about trying to modify the Python interpreter to suit your needs, I may be out of my depth here. It also is likely out of the scope of this question so you may want to open a new one to explore further – Hymns For Disco Jan 10 '20 at 09:50
  • Ok got it. It's the c based cpython implementation. Where can I find working with the ast.Str class is what I asked? Any clean working example? 'import ast' ; 'ast.Str()' – Gary Jan 10 '20 at 10:43
  • 1
    @Gary I personally haven't used python's `ast` library, and from searching a bit it does seem quite obscure, however I was able to find a site of "The missing Python AST documentation", https://greentreesnakes.readthedocs.io/en/latest/. The "examples" page may be of some help to you. As a side note, according to the [official docs](https://docs.python.org/3/library/ast.html), as of Python 3.8, `ast.Str` and others are now deprecated, superseded by `ast.Constant`. I hope this helps you a bit – Hymns For Disco Jan 10 '20 at 11:09
3

How about instrument the bytecode to add a print statement before each statement that stores to the global variable. Here is an example:

from bytecode import *

def instr_monitor_var(func, varname):
    print_bc = [Instr('LOAD_GLOBAL', 'print'), Instr('LOAD_GLOBAL', varname),
                Instr('CALL_FUNCTION', 1), Instr('POP_TOP')]

    bytecodes = Bytecode.from_code(func.__code__)
    for i in reversed(range(len(bytecodes))):
        if bytecodes[i].name=='STORE_GLOBAL' and bytecodes[i].arg==varname:
            bytecodes[i:i]=print_bc

    func.__code__=bytecodes.to_code()

def test():
    global a
    a = 1
instr_monitor_var(test, 'a')
test()

instr_monitor_var can instrument a function test so the global variable a will be printed out when its value is changed. Let me know if this works. Thanks!

0xCC
  • 1,261
  • 1
  • 10
  • 8