2

I have seen (a great) many tutorials and snippets of decorators w/ and w/o arguments, including those two I would look consider as canonical answers: Decorators with arguments, python decorator arguments with @ syntax, but I don't see why I get an error in my code.

The code below lives in the file decorators.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Description: decorators
"""
import functools

def repeat(nbrTimes=2):
    '''
    Define parametrized decorator with arguments
    Default nbr of repeats is 2
    '''
    def real_repeat(func):
        """
        Repeats execution 'nbrTimes' times
        """
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            while nbrTimes != 0:
                nbrTimes -= 1
                return func(*args, **kwargs)
        return wrapper_repeat
    return real_repeat

The first warning I get from my syntax-checker is that nbrTimes is an "unused argument".

I tested the above in python3 interactive console with:

>>> from decorators import repeat

>>> @repeat(nbrTimes=3)
>>> def greetings():
>>>     print("Howdy")
>>>
>>> greetings()
Traceback (most recent call last):
  File "<stdin>", line 1 in <module>
  File path/to/decorators.py, line xx in wrapper_repeat
   '''
UnboundLocalError: local variable 'nbrTimes' referenced before assignment.

I just don't see where I'm bungling it. In other examples the passed parameter (here nbrTimes) was not "used" until later in the inner function, so the "unused argument" warning and error upon execution leave me kind of high and dry. Still relatively new to Python. Help much appreciated.

Edit: (in response to duplicate flag by @recnac) It is not clear at all what OP in your purported duplicate wanted to achieve. I can only surmise that he/she intended to have access to a counter defined inside a decorator's wrapper, from global scope, and failed to declare it as nonlocal. Fact is we don't even know whether OP dealt with Python 2 or 3, although it is largely irrelevant here. I concede to you that the error messages were very similar, if not equivalent, if not the same. However my intent was not to access a in-wrapper-defined counter from global scope. I intended to make this counter purely local, and did. My coding errors were elsewhere altogether. It turns out the excellent discussion and solution provided by Kevin (below) are of a nature, totally different from just adding a nonlocal <var> inside the wrapper definition block (in case of Python 3.x). I won't be repeating Kevin's arguments. They are limpid and available to all.

Finally I go out on a limb and will say that the error message is perhaps the least important of all here, even though it is clearly a consequence of my bad code. For that I make amends, but this post is definitely not a rehash of the proposed "duplicate".

Cbhihe
  • 511
  • 9
  • 24
  • 2
    Aside: code is easier to read when its style is consistent. Calling something `nbrTimes` in otherwise PEP 8 formatted code is visually jarring. Consider renaming that to `nbr_times`, `number_of_times` or even just `times`. – ChrisGPT was on strike Apr 22 '19 at 12:33
  • @Chris; Sorry, I am new to this. I didn't even know that there were conventions of that sort. I tend to use whatever ink I find in my pen at time of writing. Tx for the heads-up ... – Cbhihe Apr 22 '19 at 13:39
  • 1
    You're welcome, Cbhihe. For completeness, here's a link to PEP 8 (Python's official style guide): https://www.python.org/dev/peps/pep-0008/. You don't _have to_ follow that guide, but most Python developers do. Good luck! – ChrisGPT was on strike Apr 22 '19 at 13:47

1 Answers1

6

The proposed duplicate question, Scope of variables in python decorators - changing parameters gives useful information that explains why wrapper_repeat considers nbrTimes to be a local variable, and how nonlocal might be used to make it recognize the nbrTimes defined by repeat. This would fix the exception, but I don't think it's a complete solution in your case. Your decorated function will still not repeat.

import functools

def repeat(nbrTimes=2):
    '''
    Define parametrized decorator with arguments
    Default nbr of repeats is 2
    '''
    def real_repeat(func):
        """
        Repeats execution 'nbrTimes' times
        """
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            nonlocal nbrTimes
            while nbrTimes != 0:
                nbrTimes -= 1
                return func(*args, **kwargs)
        return wrapper_repeat
    return real_repeat

@repeat(2)
def display(x):
    print("displaying:", x)

display("foo")
display("bar")
display("baz")

Result:

displaying: foo
displaying: bar

"foo" and "bar" are each displayed only one time, and "baz" is displayed zero times. I assume this is not the desired behavior.

The first two calls to display fail to repeat because of the return func(*args, **kwargs) inside your while loop. The return statement causes wrapper_repeat to terminate immediately, and no further iterations of the while will occur. So no decorated function will repeat more than once. One possible solution is to remove the return and just call the function.

import functools

def repeat(nbrTimes=2):
    '''
    Define parametrized decorator with arguments
    Default nbr of repeats is 2
    '''
    def real_repeat(func):
        """
        Repeats execution 'nbrTimes' times
        """
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            nonlocal nbrTimes
            while nbrTimes != 0:
                nbrTimes -= 1
                func(*args, **kwargs)
        return wrapper_repeat
    return real_repeat

@repeat(2)
def display(x):
    print("displaying:", x)

display("foo")
display("bar")
display("baz")

Result:

displaying: foo
displaying: foo

"foo" is being displayed twice, but now neither "bar" nor "baz" appear. This is because nbrTimes is shared across all instances of your decorator, thanks to nonlocal. once display("foo") decrements nbrTimes to zero, it remains at zero even after the call completes. display("bar") and display("baz") will execute their decorators, see that nbrTimes is zero, and terminate without calling the decorated function at all.

So it turns out that you don't want your loop counter to be nonlocal. But this means you can't use nbrTimes for this purpose. Try creating a local variable based on nbrTimes' value, and decrement that instead.

import functools

def repeat(nbrTimes=2):
    '''
    Define parametrized decorator with arguments
    Default nbr of repeats is 2
    '''
    def real_repeat(func):
        """
        Repeats execution 'nbrTimes' times
        """
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            times = nbrTimes
            while times != 0:
                times -= 1
                func(*args, **kwargs)
        return wrapper_repeat
    return real_repeat

@repeat(2)
def display(x):
    print("displaying:", x)

display("foo")
display("bar")
display("baz")

Result:

displaying: foo
displaying: foo
displaying: bar
displaying: bar
displaying: baz
displaying: baz

... And while you're at it, you may as well use a for loop instead of a while.

import functools

def repeat(nbrTimes=2):
    '''
    Define parametrized decorator with arguments
    Default nbr of repeats is 2
    '''
    def real_repeat(func):
        """
        Repeats execution 'nbrTimes' times
        """
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(nbrTimes):
                func(*args, **kwargs)
        return wrapper_repeat
    return real_repeat

@repeat(2)
def display(x):
    print("displaying:", x)

display("foo")
display("bar")
display("baz")
Kevin
  • 74,910
  • 12
  • 133
  • 166
  • _[Python 3.7.3 on Arch]_ -- Still have an issue with `UnboundLocalError: local variable 'nbrTimes' referenced before assignment`. It is the exact same error as before with code copied/pasted from directly above ... I think the difference derive from my calling `from decorators, import repeat` in the console instead of having it all in one script as is your case (I assume). I start the console from the same directory where decorators.py is located. Could it be a PYTHONPATH issue ? – Cbhihe Apr 22 '19 at 14:32
  • I tried: `@repeat()`, `@repeat(3)`, `@repeat(nbrTimes=4)` and finally `@repeat`, followed by function definition and function call... I did not expect the latter to work properly. None did. :-| – Cbhihe Apr 22 '19 at 14:35
  • 1
    Strange, I would expect my final code block to work even if you imported it into the console. `local variable 'nbrTimes' referenced before assignment` should be impossible, since I never have `nbrTimes` on the left hand side of an assignment. I suspect that your code is somehow referencing an older version of your `decorators` module. I suggest closing the console and opening a brand new one. It may also help to rename your `decorators` file. – Kevin Apr 22 '19 at 14:40
  • `@repeat` and `@repeat(3)` and `@repeat(nbrTimes=4)` should all work. I expect `@repeat` to run without crashing, but it won't actually call the decorated function. – Kevin Apr 22 '19 at 14:42
  • +1 All is well, Kevin tx very much for the gentle, baby step-by-baby step explanation. What is strange though is I **did** re-issue `from decorators import repeat` at every change and vim buffer-write of `decorators.py`, but my console session did not take that into account. It stuck with the initial import of the buggy decorator definition. (Just checked the sequence of cmds I issued in console history and the console just did not re-import.) Not the behavior I expected... – Cbhihe Apr 22 '19 at 14:49