Search code examples
pythondecorator

Change value of variable in Python decorator


from functools import wraps

class EventCounter(object):
    def __init__(self, schedules=None, matters=None):
        self.counter = 0
        self.schedules = schedules
        self.matters = matters
        if not isinstance(schedules, list) or not isinstance(matters, list):
            raise ValueError("schedules and matter must be list.")
        if not all([schedules, matters]):
            raise ValueError("Need to set schedules and matters both.")

    def __call__(self, f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            if self.schedules:
                if self.counter == self.schedules[0]:
                    self.schedules.pop(0)
                    if self.matters:
                        self.matters.pop(0)
            wrapper.counter = self.counter
            wrapper.schedule = self.schedules[0] if self.schedules else None
            wrapper.matter = self.matters[0] if self.matters else None
            self.counter += 1
            return f(*args, **kwargs)
        return wrapper

if __name__ == '__main__':
    @EventCounter([2, 4, 8, 16], [0, 1, 2, 3, 4])
    def reset():
        print(f'{reset.counter}: {reset.matter}')

    for _ in range(20):
        reset()

Is it possible to change the value of variable in decortor?

In this case, I want to reset counter to 0, like

def reset():
    print(f'{reset.counter}: {reset.matter}')
    if reset.counter == 12:
        reset.counter = 0

But the code above doesn't work for me.

Any suggestion?

Also, I want to change members of Decorator like schedules and matters

Solution:

Thanks for @ShadowRanger 's advice, inspire me a lot.

There is a conciser way to deal with this, shown as below:

from collections import deque
from functools import wraps

def EventCounter(schedules, matters):
    if not (schedules and matters):
        raise ValueError("schedules and matters must be non-empty.")

    def wrap_func(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            wrapper.schedules = deque(wrapper.schedules)
            wrapper.matters = deque(wrapper.matters)
            if wrapper.schedules and wrapper.counter == wrapper.schedules[0]:
                wrapper.schedules.popleft()
                if wrapper.matters and len(wrapper.matters) != 1: # keep the last matter
                    wrapper.matters.popleft()
            wrapper.schedule = wrapper.schedules[0] if wrapper.schedules else None
            wrapper.matter = wrapper.matters[0] if wrapper.matters else None
            wrapper.counter += 1
            return func(*args, **kwargs)

        wrapper.counter = 0  # Initialize wrapper.counter to zero before returning it
        wrapper.schedules = deque(schedules)
        wrapper.matters = deque(matters)
        return wrapper
    return wrap_func

if __name__ == '__main__':
    @EventCounter([2, 4, 8, 16], [0, 1, 2, 3])
    def reset():
        print(f'{reset.counter}: {reset.matter}')
        if reset.counter == 12:
            reset.counter = 0
            reset.schedules = [1,5,7,11]
            reset.matters = [10, 20, 30, 40, 50]

    for _ in range(30):
        reset()


Solution

  • Your problem is that ints are immutable, and you're maintaining wrapper.counter and self.counter separately, resetting wrapper.counter to self.counter on each call (undoing your attempt to reset it via the wrapper). In this case, there is no real benefit to maintaining self.counter as an instance variable (the EventCounter object is discarded after decoration completes; it technically exists thanks to closing on self in wrapper, but accessing it would be nuts; frankly, the whole class is unnecessary and all of this could be done with simple closures), so the simplest solution is to store a single copy of the counter solely on the wrapper function:

    from functools import wraps
    
    class EventCounter(object):
        def __init__(self, schedules=None, matters=None):
            # Remove definition of self.counter
            self.schedules = schedules
            self.matters = matters
            if not isinstance(schedules, list) or not isinstance(matters, list):
                raise ValueError("schedules and matter must be list.")
            if not all([schedules, matters]):
                raise ValueError("Need to set schedules and matters both.")
    
        def __call__(self, f):
            @wraps(f)
            def wrapper(*args, **kwargs):
                if self.schedules:
                    if wrapper.counter == self.schedules[0]:  # work solely in terms of wrapper.counter
                        self.schedules.pop(0)
                        if self.matters:
                            self.matters.pop(0)
                # No need to copy to/from wrapper.counter here
                wrapper.schedule = self.schedules[0] if self.schedules else None
                wrapper.matter = self.matters[0] if self.matters else None
                wrapper.counter += 1
                return f(*args, **kwargs)
            wrapper.counter = 0  # Initialize wrapper.counter to zero before returning it
            return wrapper
    
    if __name__ == '__main__':
        @EventCounter([2, 4, 8, 16], [0, 1, 2, 3, 4])
        def reset():
            print(f'{reset.counter}: {reset.matter}')
            if reset.counter == 12:
                reset.counter = 0
    
        for _ in range(20):
            reset()
    

    Try it online!