0

I'm brand-new to decorators and closures, I'm trying to practice with a simple example. When executed it raises an error of:

NameError: name 'congratulate' is not defined

What do I need to change?

"""
A recursive function to check if a string is a palindrome.
"""

@congratulate
def palindrome(phrase):
    characters = [char.lower() for char in phrase if char.isalpha()]
    chars_len = len(characters)

    out1 = characters[0]
    out2 = characters[-1]

    if chars_len <= 2:
        return out1 == out2
    else:
        if out1 == out2:
            return palindrome(characters[1:-1])
        else:
            return False


def congratulate(func):
    if func:
        print('Congratulations, it\'s a palindrome!')


if __name__ == '__main__':
    print(palindrome('Rats live on no evil star'))
Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
flybonzai
  • 3,763
  • 11
  • 38
  • 72
  • 1
    What's the difference between the expected behavior and the observed behavior? Is there an error? Does it crash? – Celeo Nov 13 '15 at 23:07
  • 1
    At least add `return func` to `congratulate()` – myaut Nov 13 '15 at 23:08
  • It says NameError: name 'congratulate' is not define. Even when I add the return function. – flybonzai Nov 13 '15 at 23:10
  • 1
    Replace 'congratulate' func at top, before 'palindrome' func. – Mikhail Gerasimov Nov 13 '15 at 23:11
  • That was it, thanks @germn! – flybonzai Nov 13 '15 at 23:13
  • @germn, out of curiosity, why does order matter in this case? – flybonzai Nov 13 '15 at 23:13
  • Replacing the order is the first step but the decorator will rename the palindrome function. meaning, when you call ```palindrome('Rats live on no evil star')``` you will get a NameError. You need to use functools.wraps to preserve the naming. see herehttp://stackoverflow.com/questions/308999/what-does-functools-wraps-do – steve Nov 13 '15 at 23:17
  • @steve, it actually runs correctly once I replace the order. Not sure why, based on what you said. – flybonzai Nov 13 '15 at 23:17
  • Are you really sure it is worth speaking about "closures" in the beginning of your question? I don't see where a closure is involved in your code or in your question. – Thomas Baruchel Nov 14 '15 at 07:30

3 Answers3

3
"""
A recursive function to check if a string is a palindrome.
"""

def congratulate(func):
    def wrapper(*argv, **kargs):
        result = func(*argv, **kargs)
        if result:
            print('Congratulations, it\'s a palindrome!')
        return result

    return wrapper

@congratulate
def palindrome(phrase):
    characters = [char.lower() for char in phrase if char.isalpha()]
    chars_len = len(characters)

    out1 = characters[0]
    out2 = characters[-1]

    if chars_len <= 2:
        return out1 == out2
    else:
        if out1 == out2:
            return palindrome(characters[1:-1])
        else:
            return False



if __name__ == '__main__':
    print(palindrome('Rats live on no evil star'))

the essence of understanding decorator is

@f
def g(args)

=>

f(g)(args)
Dyno Fu
  • 8,753
  • 4
  • 39
  • 64
  • http://scottlobdell.me/2015/04/decorators-arguments-python/ i find the blog article explains the concept very clear. – Dyno Fu Nov 13 '15 at 23:24
1

Move the congratulate() function above the function it's decorating (palindrome).

mathandy
  • 1,892
  • 25
  • 32
1

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)!

Community
  • 1
  • 1
Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
  • Thank you, this was the kind of response I needed to deepen my knowledge! – flybonzai Nov 14 '15 at 21:36
  • You're welcome, flybonzai. If you, or any other future visitor to this question, want to get a more verbose introduction to Decorators, then have a look at the advanced chapters of the 5th Edition of Learning Python by Lutz. – Dimitris Fasarakis Hilliard Nov 15 '15 at 11:33