-->

How to catch a particular name assignment?

2020-08-01 08:34发布

问题:

(Based on this question):

One can override the __setattr__ magic method for an object to have additional instructions when an attribute of an object is set. As in:

class MyClass(object):
    def __init__(self, attribute=None):
        object.__init__(self)
        self.attribute = attribute


    def __setattr__(self, name, value):
        self.__dict__[name] = value
        if name == 'attribute':
            print("attribute's value is modified to {}.".format(
                                                        self.attribute))


if __name__ == '__main__':
    my_obj = MyClass(True)
    while True:
        my_obj.attribute = input()
  • How can I catch a particular name assignment in the current script without using classes(specifically to call a method with more instructions)?

 

def b_is_modified():
    print("b is modified!")


if __name__ == '__main__':
    a = 3
    b = 4
    b = 5

How to call b_is_modified when b is assigned a value?

回答1:

I think the other answer by Nae sums it up; I'm not aware of any built-in mechanisms in the Python languages to detect assignments, so if you want an interrupt-like event system to trigger upon assignment I don't know if it's feasible.

However, you seem quiet determined to get a way to "detect" assignment, so I want to describe an approach that might get you closer than nothing.

There are the built-in functions globals() and locals() that creates dictionary of variables in global and local scope respectively. (They, in additon to vars() are further explained here).

A noteworthy point is that locals() will behave differently if called from inside a function:

If locals() is called inside a function it constructs a dictionary of the function namespace as of that moment and returns it -- any further name assignments are not reflected in the returned dictionary, and any assignments to the dictionary are not reflected in the actual local namespace

If locals() is called outside a function it returns the actual dictionary that is the current namespace. Further changes to the namespace are reflected in the dictionary, and changes to the dictionary are reflected in the namespace:

Here is a "hacky" way to detect changes to variables:

def b_is_modified():
    print("b is modified!")


if __name__ == '__main__':
    old = locals().get('b')

    a = 3
    b = 4
    b = 5

    new = locals().get('b')
    if id(new) != id(old) and new is not None:
        b_is_modified()

This is nothing else but an (obfuscated?) way of checking if the value of b has changed from one point in execution to another, and there is no callback event or trigger action that detects it. However, if you want to expand on this approach continue reading.

The rest of the answer explains how to check for changes in b by rewriting it to something like:

if __name__ == '__main__':
    monitor = ScopeVariableMonitor(locals())

    a = 3
    b = 4
    monitor.compare_and_update()  # Detects creation of a and b
    b = 5
    monitor.compare_and_update()  # Detects changes to b

The following will "detect" any changes to the variables, and I've also included an example where it's used inside a function, to reiterate that then the dictionary returned from locals() does not update.

The ScopeVariableMonitor-class is just an example, and combines the code in one place. In essence, it's comparing changes to the existence and values of variables between update()s.

class ScopeVariableMonitor:
    def __init__(self, scope_vars):
        self.scope_vars = scope_vars  # Save a locals()-dictionary instance
        self.old = self.scope_vars.copy()  # Make a shallow copy for later comparison

    def update(self, scope_vars=None):
        scope_vars = scope_vars or self.scope_vars
        self.old = scope_vars.copy()  # Make new shallow copy for next time

    def has_changed(self, var_name):
        old, new = self.old.get(var_name), self.scope_vars.get(var_name)
        print('{} has changed: {}'.format(var_name, id(old) != id(new)))

    def compare_and_update(self, var_list=None, scope_vars=None):
        scope_vars = scope_vars or self.scope_vars
        # Find new keys in the locals()-dictionary
        new_variables = set(scope_vars.keys()).difference(set(self.old.keys()))
        if var_list:
            new_variables = [v for v in new_variables if v in var_list]
        if new_variables:
            print('\nNew variables:')
            for new_variable in new_variables:
                print(' {} = {}'.format(new_variable, scope_vars[new_variable]))

        # Find changes of values in the locals()-dictionary (does not handle deleted vars)
        changed_variables = [var_name for (var_name, value) in self.old.items() if
                             id(value) != id(scope_vars[var_name])]
        if var_list:
            changed_variables = [v for v in changed_variables if v in var_list]
        if changed_variables:
            print('\nChanged variables:')
            for var in changed_variables:
                print('  Before: {} = {}'.format(var, self.old[var]))
                print(' Current: {} = {}\n'.format(var, scope_vars[var], self.old[var]))
        self.update()

The "interesting" part is the compare_and_update()-method, if provided with a list of variables names, e.g. ['a', 'b'], it will only look for changes to those to variables. The scope_vars-parameter is required when inside the function scope, but not in the global scope; for reasons explained above.

def some_function_scope():
    print('\n --- Now inside function scope --- \n')
    monitor = ScopeVariableMonitor(locals())
    a = 'foo'
    b = 42
    monitor.compare_and_update(['a', 'b'], scope_vars=locals())
    b = 'bar'
    monitor.compare_and_update(scope_vars=locals())


if __name__ == '__main__':
    monitor = ScopeVariableMonitor(locals())
    var_list = ['a', 'b']
    a = 5
    b = 10
    c = 15
    monitor.compare_and_update(var_list=var_list)

    print('\n *** *** *** \n')  # Separator for print output

    a = 10
    b = 42
    c = 100
    d = 1000
    monitor.has_changed('b')
    monitor.compare_and_update()

    some_function_scope()

Output:

New variables:
 a = 5
 b = 10

 *** *** *** 

b has changed: True

New variables:
 d = 1000

Changed variables:
  Before: b = 10
 Current: b = 42

  Before: a = 5
 Current: a = 10

  Before: c = 15
 Current: c = 100


 --- Now inside function scope --- 

New variables:
 a = foo
 b = 42

Changed variables:
  Before: b = 42
 Current: b = bar

Conclusion

My answer is just a more general way of doing:

b = 1
old_b = b

# ...

if b != old_b:
    print('b has been assigned to')

The dictionary from locals() will hold everything that is a variable, including functions and classes; not just "simple" variables like your a, b and c.

In the implementation above, checks between "old" and "new" values are done by comparing the id() of the shallow copy of before with id() of the current value. This approach allows for comparison of ANY value, because the id() will return the virtual memory address, but this is assumable far from a good, general comparison scheme.

I'm curious to what you want to achieve and why you want to detect assignments: if you share your goal then perhaps I can think of another way to reach it in another way.



回答2:

Based on this answer:

It can't be catched(at least in python level).

Simple name assignment(b = 4), as oppposed to object attribute assignment (object.b = 5), is a fundamental operation of the language itself. It's not implemented in terms of a lower-level operation that one can override. Assignment just is.