Search code examples
python-decoratorspython-classpython-closures

Decorator only works when method is decorated but not when the decorator is called with method passed as argument


Long-time listener; first time caller, so please don't shoot me if I've not perfectly followed the guidelines.

So I'm trying to create a decorator which applies a status bar to a generator method. My decorator works just fine when using the @decorator syntax. When calling the decorator and passing a method as the argument, however, the decorator does nothing at all. I'd like to call the decorator function just like any other function rather than use @status_bar because I want a simple means of using the generator optionally.

Here is the working code:

import time

from alive_progress import alive_bar


def status_bar(func):

    def wrapper(self):
        with alive_bar() as bar:
            for x in func(self):
                bar()

    return wrapper


class TestClass:

    def __init__(self):
        pass

    @status_bar
    def iter_range(self):
        for x in range(10000):
            time.sleep(0.01)
            yield x


TestClass().iter_range()

This yields a status bar in the prompt as expected.

| ▶▶▶▶▶▶▶▶▶▶▶▶▶ | ▂▂▄ 326 in 3s (98.2/s)

But running the code without the syntactic sugar fails to do anything.

def status_bar(func):

    def wrapper(self):
        with alive_bar() as bar:
            for x in func(self):
                bar()

    return wrapper


class TestClass:

    def __init__(self):
        pass

    def iter_range(self):
        for x in range(10000):
            time.sleep(0.01)
            yield x


status_bar(TestClass().iter_range)

Instead, the code runs as though no functions have been called.

Process finished with exit code 0

I always seem to get the same output, and I know there's a simple explanation here, which is why it's bugging me so much that I can't get it to work. Any help is greatly appreciated. Thanks.


Solution

  • I'm trying to learn this myself. Using https://stackoverflow.com/a/25827070/59867 as a reference, I figured out that:

    When you call status_bar(TestClass().iter_range), that returns the decorated function which you're not actually calling, which is why nothing is happening.

    So calling it similarly, but just passing in the function from the class (and not from an instance), this works:

    decorated_func = status_bar(TestClass.iter_range)
    decorated_func(TestClass())
    

    Which means this is how you'd call it:

    status_bar(TestClass.iter_range)(TestClass())