Search code examples
pythonpython-3.xdecoratorpython-decorators

decorator: understanding why it does not flush local variables


I've written a simple decorator:

from functools import wraps
import random

def my_dec(f):
    lst = list()

    @wraps(f)
    def wrapper(*args):
        lst.append(random.randint(0, 9))
        print(lst)
        return f(*args)

    return wrapper

@my_dec
def foo():
    print("foo called")

Now, if I call foo multiple times lst is not being flushed. Instead, it builds up over time. Thus, multiple calls of foo return an output like this:

foo()
> [4]
> foo called

foo()
> [4, 9]
> foo called

foo()
> [4, 9, 1]
> foo called

...

Why is that? I thought a decorator is just syntactic sugar for my_dec(foo)?! I assumed that each call to my_dec flushes lst.


Solution

  • You're right... The decorator is just syntactic sugar. Specifically:

    @decorator
    def foo():
        pass
    

    is exactly the same thing as:

    def foo():
        pass
    foo = decorator(foo)
    

    Let's be a little more outlandish and rewrite this another way that is mostly equivalent1:

    def bar():
        pass
    foo = decorator(bar)
    del bar
    

    Hopefully written out this way, you can see that if I call foo a bunch of times, I'm not calling decorator a bunch of times. decorator only got called once (to help create foo).

    Now in your example, your decorator creates a list immediately when it gets called:

    def my_dec(f):
        lst = list()  # list created here!
    
        @wraps(f)
        def wrapper(*args):
            lst.append(random.randint(0, 9))
            print(lst)
            return f(*args)
    
        return wrapper
    

    The function returned wrapper gets assigned to your foo, so when you call foo, you're calling wrapper. Note that there is no code in wrapper that would reset lst -- only code that would add more elements to lst so there is nothing here to indicate the lst should get "flushed" between calls.

    1(depending on what the decorator does, you might see some differences in the function's __name__ attribute, but otherwise it's the same thing...)


    Also note that you'll have one lst for each time the decorator is called. We can go crazy with this one if we like and decorate foo twice:

    @my_dec
    @my_dec
    def foo():
        pass
    

    Or we can decorate more than one function:

    @my_dec
    def foo():
        pass
    
    @my_dec
    def bar():
        pass
    

    And then when we call foo and bar, we'll see that they each accumulate their own (distinct) lists of random numbers. In other words, each time your decorator is applied to something, a new list will be created and each time that "something" is called, the list will grow.