6

I'm just starting with Python and I have just been exposed to decorators. I wrote the following code, mimicking what I am seeing, and it works:

def decorator_function(passed_function):
    def inner_decorator():
        print('this happens before')
        passed_function()
        print('this happens after')
    return inner_decorator

@decorator_function
def what_we_call():
    print('The actual function we called.')

what_we_call()

But then I wrote this, which throws errors:

def decorator_function(passed_function):
    print('this happens before')
    passed_function()
    print('this happens after')

@decorator_function
def what_we_call():
    print('The actual function we called.')

what_we_call()

So, why do we need to have that inner nested function inside the decorator function? what purpose does it serve? Wouldn't it be simpler to just use the syntax of the second? What am I not getting?

The funny thing is that BOTH have the same (correct) output, but the second on has error text as well, saying "TypeError: 'NoneType' object is not callable"

Please use language and examples suitable for someone just starting with Python, his first programming language - and also new to OOP as well! :) Thanks.

Sindyr
  • 1,277
  • 1
  • 11
  • 16

2 Answers2

8

The reason is that when you wrap what_we_call in decorator_function by doing:

@decorator_function
def what_we_call():
    ...

What you're doing is:

what_we_call = decorator_function(what_we_call)

In you first example it works because you don't run the inner_function actually, you only initialise it, and then you return the new inner_function back (that you will call later when call the decorated what_we_call):

def decorator_function(passed_function):
    def inner_decorator():
        print('this happens before')
        passed_function()
        print('this happens after')
    return inner_decorator

Contrarily, in your second example you're going to run 2 print statements and the passed_function (what_we_call in our case) in the between:

def decorator_function(passed_function):
    print('this happens before')
    passed_function()
    print('this happens after')

In other words, you don't return a function in the example of before:

what_we_call = decorator_function(what_we_call)

You run the code (and you see the output), but then decorator_function returns 'None' to what_we_call (overwriting the original function), and when you call 'None' as if it was a function Python complains.

matteo
  • 459
  • 4
  • 10
  • So the decorator_function is merely the "engine" that gets called whenever what_we_cal() is invoked, but the inner_decorator is the actual function that gets passed back? So I could pass back anything via return, so long as it is a function? – Sindyr Aug 25 '15 at 00:09
  • So you could even write this: `def decorator_function(passed_function): return other_function` and ellipse the decorated function entirely, yes? – Sindyr Aug 25 '15 at 00:11
  • 1
    Yes to both questions (if with 'ellipse' you mean that passed_function is throw off and not used at all, because substituted by other_function). About the code formatting in comment I don't know :) – matteo Aug 25 '15 at 00:18
  • Hmmm... so adding the @decorator_name above the function basically says "Hey I know you want to use this function, but instead run this other function called decorator_name and use the function that IT tells you to (or returns) in place of the original one you called". And the function the decorator passes back could include the original function or not. Is that about right? – Sindyr Aug 25 '15 at 00:49
  • Yes, particularly the @decorator_name before the def of a function also pass the original function itself as a parameter to the decorated one, in order to "expand" the original function (usually). Anyway yes, you can also discard the passed function and return a new one without any relation with the one passed as argument. – matteo Aug 25 '15 at 06:17
  • Is the question answered? – matteo Aug 25 '15 at 22:15
4

Python decorators are basically just syntactic sugar. This:

@decorator
def fn(arg1, arg2):
    return arg1 + arg2

Becomes this:

def fn(arg1, arg2):
    return arg1 + arg2
fn = decorator(fn)

That is, a decorator basically accepts a function as an argument, and returns "something"; that "something" is bound to the name of the decorated function.

In nearly all cases, that "something" should be another function, because it is expected that fn will be a function (and will probably be called as though it is).

mipadi
  • 398,885
  • 90
  • 523
  • 479