How a decorator can register a function without a formal call?

Question:

I am looking at one of the examples from here, in particular at the following one:

import random
PLUGINS = dict()

def register(func):
    """Register a function as a plug-in"""
    PLUGINS[func.__name__] = func
    return func

@register
def say_hello(name):
    return f"Hello {name}"

@register
def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def randomly_greet(name):
    greeter, greeter_func = random.choice(list(PLUGINS.items()))
    print(f"Using {greeter!r}")
    return greeter_func(name)

What stucks me is that neither say_hello() nor be_awesome() functions are called until the very last line, but PLUGINS already contains both of them. I am used to think that decorators are applied while the functions are explicitly called, but this example tells me that I was wrong. Does this happen because register function does not call func? But what if it does call? Is this a special case to remember or is there some system behind it?

Asked By: Ivan

||

Answers:

@register
def say_hello(name):
    return f"Hello {name}"

is equivalent to:

def say_hello(name):
    return f"Hello {name}"

say_hello = register(say_hello)
Answered By: Peter Wood

I know it’s an old question, but there is an explanation for the mentioned behavior. In Python, decorators are executed the moment their decorated functions are declared during module import (known as import time). This behavior can be seen in the following example:

registry = []

# decorator
def register(func):
  print(f"Function {func} passing here")
  registry.append(func)
  return func

@register
def earth():
  print("Hello earth!")

@register
def mars():
  print("Hello Mars!")

# not decorated
def venus():
  print("Hello Venus!")

def main():
  print("Starting the main function")
  earth()
  mars()
  venus()
  print(registry[:])

if __name__ == "__main__":
  main()

The output will be:

Object <function earth at 0x7f33c7fd1c10> passing here
Object <function mars at 0x7f33d81c75e0> passing here
Starting the main function
Hello earth!
Hello Mars!
Hello Venus!
[<function earth at 0x7f33c7fd1c10>, <function mars at 0x7f33d81c75e0>]

As you can see, the decorator is executed when each function is defined during import (import time), but the function itself is not executed unless it is called.

If anyone wants to go deeper into this concept, I recommend the book Fluent Python by Luciano Ramalho. The code above is a variation of the example found there.

Answered By: WesFMan