6.2. Decorator Mechanics

6.2.1. Recap

  • When you define a function, Python will do several things

  • Python will create a function object

  • Python will compile function body to bytecode and assign it to the funcobj.__code__.co_code

  • Python will add function name (identifier) to the globals() (name will be a reference of the function object)

  • When you call a function, Python will call funcobj.__call__() method which will execute it's bytecode (funcobj.__code__.co_code)

Definition:

>>> def hello():
...     return 'hello'

Python will create a function object:

>>> hello  
<function hello at 0x107f9e660>

Python will compile function body to bytecode and assign it to the funcobj.__code__.co_code:

>>> hello.__code__.co_code
b'\x95\x00g\x01'

Python will add function name (identifier) to the globals() (name will be a reference of the function object):

>>> 'hello' in  globals()
True
>>> globals()['hello']  
<function hello at 0x107f9e660>

When you call a function, Python will call funcobj.__call__() method which will execute it's bytecode (funcobj.__code__.co_code):

>>> hello.__call__()
'hello'

6.2.2. Function Definition

Let's define a simple function:

>>> def hello():
...     return 'hello'

Now we can call the function:

>>> hello()
'hello'

6.2.3. Create an Alias

Let's define a simple function:

>>> def hello():
...     return 'hello'

Now we can call the function:

>>> hello()
'hello'

We can also define an alias to the function:

>>> alias = hello
>>> alias()
'hello'

Why this works:

>>> hello 
<function hello at 0x107f9e660>
>>>
>>> alias 
<function hello at 0x107f9e660>

6.2.4. Helper Function

  • debug content was executed at the moment of decorating the function (not calling it)

Let's define a simple function:

>>> def debug(func):
...     print('debug')
...     return func
>>>
>>> def hello():
...     return 'hello'

Now we can call the function:

>>> hello()
'hello'

Creating alias will execute extra steps:

>>> alias = debug(hello)
debug
>>>
>>> alias()
'hello'

Note, that the debug content was executed at the moment of decorating the function (not calling it). If you want to delay this to the moment of a call, we need to introduce a one more step.

Both hello and alias points to the same object:

>>> hello 
<function hello at 0x107f9e660>
>>>
>>> alias 
<function hello at 0x107f9e660>

6.2.5. Delay Execution

  • We need to delay the execution of the debug function content

>>> def debug(func):
...     def on_call():
...         print('debug')
...         return func()
...     return on_call
>>>
>>> def hello():
...     return 'hello'

Calling hello works as usual:

>>> hello()
'hello'

Creating alias will execute extra steps:

>>> alias = debug(hello)
>>> alias()
debug
'hello'

This time hello and alias points to the different object. Identifier hello points as usual to the function object, but alias points to the on_call function object, which is a wrapper around the original function. Upon a call, on_call it will execute the extra steps and then call the original function.

>>> hello 
<function hello at 0x107f9e660>
>>>
>>> alias 
<function debug.<locals>.on_call at 0x107f9c360>

6.2.6. Make New Behavior Permanent

  • If we define alias with the same name as the original function

  • globals()['hello'] will point not to hello() but to debug() function

  • This will make the new behavior permanent

>>> def debug(func):
...     def on_call():
...         print('debug')
...         return func()
...     return on_call
>>>
>>> def hello():
...     return 'hello'

Calling hello works as usual:

>>> hello()
'hello'
>>>
>>> hello  
<function hello at 0x107f9e660>

If we define alias with the same name as the original function, the globals()['hello'] will point not to hello() but to debug() function instead. This will make the new behavior permanent. Whenever someone calls the hello() he/she will get a new behavior:

>>> hello = debug(hello)
>>> hello()
debug
'hello'

This is because the hello identifier now points to the debug:

>>> hello  
<function debug.<locals>.on_call at 0x107f9c360>

6.2.7. Decorator Syntax

  • @debug is just a shortcut to hello = debug(hello)

>>> def debug(func):
...     def on_call():
...         print('debug')
...         return func()
...     return on_call
>>>
>>> @debug
... def hello():
...     return 'hello'

Calling hello won't work as usual. This time it will execute the on_call body, adding few extra steps.

>>> hello()
debug
'hello'