I know I'm late to the party, but I want to expand.
As noted, the NameError
in this case is caused by the fact that you use a name before you actually create one. Moving congratulate()
to the top remedies this.
Appart from the NameError
you have two implicit Logic Errors relating to Decorator/Function Functionality:
First Issue:
- Your
if
clause in congratulate
always evaluates to True
; you aren't exactly congratulating when a string is a palindrome.
This is caused by the fact that function objects always evaluate to True
, so a condition of the form if func:
will always execute:
def f():
pass
if f:
print("I'm true!")
# Prints: I'm true!
This is thankfully trivial and can easily be fixed by actually calling the function if func("test string"):
Second Issue:
- The second issue here is less trivial and probably caused by the fact that decorators can be comfusing. You aren't actually using
congratulate()
the way decorators are supposed to be used.
A decorator is a callable that returns a callable (callables are things like functions, classes overloaded on __call__
). What your 'decorator' is doing here is simply accepting a function object, evaluating if the object is True
and then printing congratulations.
Worst part? It is also implicitly rebinding the name palindrome
to None
.
Again, you can see this indirect effect (+1 for rhyming) in this next snippet:
def decor(f):
if f: print("Decorating can be tricky")
@decor
def f():
print("Do I even Exist afterwards?")
# When executed, this prints:
Decorating can be tricky
Cool, our function f
has been decorated, but, look what happens when we try calling our function f
:
f()
TypeError Traceback (most recent call last)
<ipython-input-31-0ec059b9bfe1> in <module>()
----> 1 f()
TypeError: 'NoneType' object is not callable
Yes, our function object f
has now been assigned to None
, the return value of our decor
function.
This happens because as pointed out, the @syntax
is directly equivalent to the following:
@decor
def f(): pass
# similar to
f = decor(f) # we re-assign the name f!
Because of this we must make sure the return value of a decorator is an object that can afterwards be called again, ergo, a callable object.
So what do you do? One option you might consider would be simply returning the function you passed:
def congratulate(func):
if func("A test Phrase!"):
print('Congratulations, it\'s a palindrome!')
return func
This will guarantee that after the decorator runs on your palindrome()
function, the name palindrome
is still going to map to a callable object.
The problem? This turns out to be a one-time ride. When Python encounters your decorator and your function, it's going to execute congratulate
once and as a result only going to execute your if
clause once.
But you need it to run this if
every time your function is called! What can you do in order to accomplish this? Return a function that executes the decorated function (so called nested function decorators).
By doing this you create a new function for the name palindrome
and this function contains your original function which you make sure is executed each time palindrome()
is called.
def congratulate(func): # grabs your decorated function
# a new function that uses the original decorated function
def newFunc():
# Use the function
if func("Test string"):
print('Congratulations, it\'s a palindrome!')
# Return the function that uses the original function
return newFunc
newFunc
is now a function that issues calls to your original function.
The decoration process now assigns the palindrome
name to the newFunc
object (notice how we returned it with return newFunc
.
As a result, each time you execute a call of the form palindrome()
this is tranlated to newFunc()
which in turn calls func()
in its body. (If you're still with me I commend you).
What's the final issue here? We've hard-coded the parameters for func
. As is, everytime you call palindrome()
function newFunc()
will call your original function func
with a call signature of func("Test String")
, which is not what we want, we need to be able to pass parameters.
What's the solution? Thankfully, this is simple: Pass an argument to newFunc()
which will then pass the argument to func()
:
def congratulate(func): # grabs your decorated function
# a new function that uses the original decorated function
# we pass the required argument <phrase>
def newFunc(phrase):
# Use the function
# we use the argument <phrase>
if func(phrase):
print('Congratulations, it\'s a palindrome!')
# Return the function that uses the original function
return newFunc
Now, everytime you call palindrome('Rats live on no evil star')
this will translate to a call of newFunc('Rats live on no evil star')
which will then transfer that call to your func as func('Rats live on no evil star')
in the if
clause.
After execution, this works wonderfully and get's you the result you wanted:
palindrome('Rats live on no evil star')
Congratulations, it's a palindrome!
I hope you enjoy reading, I believe I'm done (for now)!