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.
- 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
- global
- nonlocal
- local
- A tuple of variables that are no longer in scope, but that a function needs in order to run.
-
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>
- 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
- 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
@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
.__doc__
.__name__
.__defaults__
-
.__wrapped__
: the original function
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)
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!