1
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()

3
  • 1
    if you want to do that I think you need to store the counter on f instead of wrapper. maybe other changes needed too Commented Jun 7, 2022 at 14:48
  • @Anentropic: Putting it on f will mean it's not an attribute of the decorated reset function. It should be on wrapper (otherwise they'll have to drill through the resulting reset [really wrapper] to find the original function to reset it), it just shouldn't also be on the EventCounter instance. Commented Jun 7, 2022 at 14:55
  • Side-note: Assuming you're not writing code to run on Python 2 (and no one really should be nowadays), you can simplify class EventCounter(object): to just class EventCounter:. Explicitly inheriting from object was only needed on Python 2 to distinguish new-style classes from old-style classes; Python 3 only has new-style, so you inherit from object implicitly anyway. Commented Jun 7, 2022 at 15:04

1 Answer 1

3

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!

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks a lot! Another question is whether can I change schedules and matters ?
As written, not easily (you'd have to drill through the closure scope stuff to get it). But you could do wrapper.schedules = self.schedules and wrapper.matters = self.matters in __call__, after defining wrapper, just like I added the wrapper.counter = 0. This will make them aliases of each other; since they're both lists, as long as you don't rebind the either attribute (only using mutating operations like method calls and indexing/slice operations, not reassignment), they'll refer to the same list forever.
@NEET: Just for completeness, after declaring your class unnecessary, the purely closure-based version of your code (also using deque for efficient popleft)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.