13 03 Decorators - HannaAA17/Data-Scientist-With-Python-datacamp GitHub Wiki

Decorators are an extremely powerful concept in Python. They allow you to modify the behavior of a function without changing the code of the function itself. This chapter will lay the foundational concepts needed to thoroughly understand decorators (functions as objects, scope, and closures), and give you a good introduction into how decorators are used and defined.

Functions are objects

  • as variables
  • lists and dictionaries of functions
  • referencing a function
def my_function():
  return 42

x = my_function
my_function() # 42

my_function #<function my_function at ...>
  • as arguments
  • define a function inside another
  • functions as return values
def get_function():
  def print_me(s):
    print(s)
  
  return print_me

Scope

  • global
  • nonlocal
  • local

Closures

  • A tuple of variables that are no longer in scope, but that a function needs in order to run.

Attaching nonlocal variables to nested functions

  • type(func.__closure__) : <class 'tuple'>
  • len(func.__closure__): number of variables stored
  • func.__closure__[0].cell_contents
def my_special_function():
  print('You are running my_special_function()')
  
def get_new_func(func):
  def call_func():
    func()
  return call_func

new_func = get_new_func(my_special_function)

# Redefine my_special_function() to just print "hello"
def my_special_function():
  print('hello')

new_func() #You are running my_special_function()
# Delete my_special_function()
del(my_special_function)

new_func() #You are running my_special_function()
# Overwrite `my_special_function` with the new function
my_special_function = get_new_func(my_special_function)

my_special_function() #You are running my_special_function()
my_special_function.__closure__[0].cell_contents
Out[6]: <function __main__.my_special_function>

Decorators

  • A wrapper that you can place around a function that changes that function's behavior.
  • @decorator = my_func = decorator(my_func)
def print_before_and_after(func):
  def wrapper(*args):
    print('Before {}'.format(func.__name__))
    # Call the function being decorated with *args
    func(*args)
    print('After {}'.format(func.__name__))
  # Return the nested function
  return wrapper

@print_before_and_after
def multiply(a, b):
  print(a * b)

multiply(5, 10)
<script.py> output:
    Before multiply
    50
    After multiply

Real-world examples

When to use decorators

  • Add common behavior to multiple functions
import time

def timer(func):
  """A decorator that prints how long a function took to run."""
  # Define the wrapper function to return.
  def wrapper(*args, **kwargs):
    # When wrapper() is called, get the current time.
    t_start = time.time()
    # Call the decorated function and store the result.
    result = func(*args, **kwargs)
    # Get the total time it took to run, and print it.
    t_total = time.time() - t_start
    print('{} took {}s'.format(func.__name__, t_total))
    return result 
  return wrapper
@timer
def sleep_n_seconds(n):
  time.sleep(n)

sleep_n_seconds(5)
# sleep_n_seconds took 5.0050950050354s
def memoize(func):
  """Store the results of the decorated function for fast lookup """
  # Store results in a dict that maps arguments to results
  cache = {}
  # Define the wrapper function to return.
  def wrapper(*args, **kwargs):
    # If these arguments haven't been seen before,
    if (args, kwargs) not in cache:
    # Call func() and store the result.
      cache[(args, kwargs)] = func(*args, **kwargs)
    return cache[(args, kwargs)] 
  return wrapper

Decorators and metadata

Preserving docstrings when decorating functions

  • @wraps(func)
from functools import wraps

def add_hello(func):
  # Decorate wrapper() so that it keeps func()'s metadata
  @wraps(func)
  def wrapper(*args, **kwargs):
    """Print 'hello' and then call the decorated function."""
    print('Hello')
    return func(*args, **kwargs)
  return wrapper
  
@add_hello
def print_sum(a, b):
  """Adds two numbers and prints the sum"""
  print(a + b)
  
print_sum(10, 20)
print(print_sum.__doc__)
<script.py> output:
    Hello
    30
    Adds two numbers and prints the sum

Access to the original function

  • .__doc__
  • .__name__
  • .__defaults__
  • .__wrapped__: the original function

Decorators that take arguments

def run_n_times(n):
   """Define and return a decorator"""
   def decorator(func):
     def wrapper(*args, **kwargs):
       for i in range(n):
         func(*args, **kwargs)
     return wrapper
   return decorator
@run_n_times(3)
def print_sum(a,b)
  print(a + b)
print(3, 5)

Timeout(): a real-world example

background info

import signal
def raise_timeout(*args, **kwargs):
   raise TimeoutError()
# When an "alarm" signal goes off, call raise_timeout()
signal.signal(signalnum=signal.SIGALRM, handler=raise_timeout)
# Set off an alarm in 5 seconds
signal.alarm(5)
# Cancel the alarm
signal.alarm(0)
def timeout(n_seconds): 
  def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs): 
      # Set an alarm for n seconds
      signal.alarm(n_seconds)
      try:
        # Call the decorated func 
        return func(*args, **kwargs)
      finally:
        # Cancel alarm 
        signal.alarm(0)
    return wrapper 
  return decorator
@timeout(5)
def foo():
  time.sleep(10)
  print('foo!')

@timeout(20)
def bar():
  time.sleep(10)
  print('bar!)

foo() #TimeoutError
bar() # bar!
⚠️ **GitHub.com Fallback** ⚠️