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".
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")