pythondecoratorpython-nonlocal

Python nonlocal variable value retained after multiple decorator calls


Sorry for the disturbing title, but I can't put the right words on what I've been looking at for hours now.

I'm using a decorator with optional parameters and i want to alter one of them within the wrapped function so that each call ends up doing a different thing. For context (that I had to remove), i want to create some sort of hash of the original function args and work with that.

from functools import  wraps


def deco(s=None):
    def _decorate(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(locals())
            
            nonlocal s  # Get the deco func arg
            if not s:
                s = "some_value_depending_on_other_args_i_removed"
            
            # do_smth_smart(s)           

            # s=None  # This solves my issue if uncommented
            return None

        return wrapper
    return _decorate

Here is a small test sample :

@deco()
def test1(self,x):
    pass
    
@deco()
def test2(self,a,b):
    pass

test1(1)
test1(2)

test2(3,4)
test2(5,6)

I would expect s to be "reset" to None whenever I call the decorated functions.

To my surprise, as it stands, the output is :

{'args': (1,), 'kwargs': {}, 's': None}
{'args': (2,), 'kwargs': {}, 's': 'newname'}
{'args': (3, 4), 'kwargs': {}, 's': None}
{'args': (5, 6), 'kwargs': {}, 's': 'newname'}

Would someone enlighten me please ? Thanks in advance :)


Solution

  • If we add some print statements:

    from functools import  wraps
    
    def deco(s=None):
        print('deco')
        def _decorate(func):
            print('_decorate')
            @wraps(func)
            def wrapper(*args, **kwargs):
                print('wrapper')
                print(locals())
                
                nonlocal s  # Get the deco func arg
                if not s:
                    s = "some_value_depending_on_other_args_i_removed"
                
                # do_smth_smart(s)           
    
                # s=None  # This solves my issue if uncommented
                return None
    
            return wrapper
        return _decorate
    
    @deco()
    def test1(self,x):
        pass
        
    @deco()
    def test2(self,a,b):
        pass
    
    test1(1)
    test1(2)
    
    test2(3,4)
    test2(5,6)
    

    We can see that deco (and _decorate) is only called when decorating the functions test1 and test2:

    deco
    _decorate
    deco
    _decorate
    wrapper
    {'args': (1,), 'kwargs': {}, 's': None}
    wrapper
    {'args': (2,), 'kwargs': {}, 's': 'some_value_depending_on_other_args_i_removed'}
    wrapper
    {'args': (3, 4), 'kwargs': {}, 's': None}
    wrapper
    {'args': (5, 6), 'kwargs': {}, 's': 'some_value_depending_on_other_args_i_removed'}
    

    Even here only wrapper is really called every time along with the function it's wrapping, so that's where s needs to be; to achieve what you're asking we can do something like assigning _s's __default__ value to be s in wrapper's parameter definition:

    from functools import  wraps
    
    def deco(s=None):
        print('deco')
        def _decorate(func):
            print('_decorate')
            @wraps(func)
            def wrapper(*args, _s=s, **kwargs):
                print('wrapper')
                print(locals())
                
                nonlocal s  # Get the deco func arg
                if not _s:
                    _s = "some_value_depending_on_other_args_i_removed"
                print(locals())
                # do_smth_smart(s)           
    
                # s=None  # This solves my issue if uncommented
                return None
    
            return wrapper
        return _decorate
    
    @deco(1)
    def test1(self,x):
        pass
        
    @deco()
    def test2(self,a,b):
        pass
    
    test1(1)
    test1(2)
    
    test2(3,4)
    test2(5,6)
    

    Outputs:

    deco
    _decorate
    deco
    _decorate
    wrapper
    {'_s': 1, 'args': (1,), 'kwargs': {}, 's': 1}
    {'_s': 1, 'args': (1,), 'kwargs': {}, 's': 1}
    wrapper
    {'_s': 1, 'args': (2,), 'kwargs': {}, 's': 1}
    {'_s': 1, 'args': (2,), 'kwargs': {}, 's': 1}
    wrapper
    {'_s': None, 'args': (3, 4), 'kwargs': {}, 's': None}
    {'_s': 'some_value_depending_on_other_args_i_removed', 'args': (3, 4), 'kwargs': {}, 's': None}
    wrapper
    {'_s': None, 'args': (5, 6), 'kwargs': {}, 's': None}
    {'_s': 'some_value_depending_on_other_args_i_removed', 'args': (5, 6), 'kwargs': {}, 's': None}