# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# + [markdown] heading_collapsed=true
# # Introduction and Background
#
# By definition, a **decorator** is a function that takes another function and extends the behavior of the latter function without explicitly modifying it. Decorators provide a simple syntax for calling [higher-order functions](https://en.wikipedia.org/wiki/Higher-order_function).

# + [markdown] heading_collapsed=true hidden=true
# ## Functions
#
# There are two kinds of functions:
# 1. **First-order function**:
#      - Takes one or more non-function objects, like string, int, float, list, and so on, as arguments and return non-function objects
#
# 2. **Higher-order function**: 
#     - takes one or more functions as arguments (i.e. a procedural parameter, which is a parameter of a procedure that is itself a procedure); 
#     - returns a function as its result.
#     - In mathematics higher-order functions are also termed operators or functionals
#     
# 3. **Inner Functions**:
#     - Defined inside other functions
#     - they are locally scoped to their parents, and only exist inside the parent() function as local variables
#     - the inner functions are not defined until the parent function is called
#
#
# **First-Class Objects**:
# In Python, functions are first-class objects. This means that functions can be passed around and used as arguments, just like any other object (string, int, float, list, and so on). Consider the following three functions:

# + hidden=true
## first and higher function Examples###

# first-order function
def say_hello(name):
    return f"Hello {name}"

# first-order function
def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

# higher-order function taking a function as parameter
def greet_bob(greeter_func):
    return greeter_func("Bob")


# + hidden=true
# Note that greet_bob(say_hello) refers to two functions, but in different ways: greet_bob() and say_hello. 
# The say_hello function is named without parentheses. This means that only a reference to the function is passed. 
# The function is not executed. The greet_bob() function, on the other hand, is written with parentheses, 
# so it will be called as usual.
greet_bob(say_hello)

# + hidden=true
greet_bob(be_awesome)


# + hidden=true
## inner function Examples###

def parent():
    print("Printing from the parent() function")
    
    # inner function
    def first_child():
        print("Printing from the first_child() function")
    
    # inner function
    def second_child():
        print("Printing from the second_child() function")

    second_child()
    first_child()


# + hidden=true
# Note that the order in which the inner functions are defined does not matter. 
# Like with any other functions, the printing only happens when the inner functions are executed.
parent()


# + hidden=true
## Returning Functions From Functions

def parent(num):
    def first_child():
        return "Hi, I am Emma"

    def second_child():
        return "Call me Liam"

    if num == 1:
        return first_child
    else:
        return second_child


# + [markdown] hidden=true
# Note that you are returning first_child without the parentheses. Recall that this means that you are **returning a reference to the function** first_child. In contrast first_child() with parentheses refers to the result of evaluating the function. This can be seen in the following example

# + hidden=true
first = parent(1)
second = parent(2)
print(f"first: {first}")
print(f"second: {second}")
print(first())
print(second())


# + [markdown] heading_collapsed=true hidden=true
# ## My First Decorators
#
# Now that you’ve seen that functions are just like any other object in Python, you’re ready to move on and see the magical beast that is the Python decorator. Let’s start with an example:

# + hidden=true
def my_first_decorator(func):
    def wrapper():
        """
        Define a inner wrapper function wraping an argument functor 
        with pre-defined behaviors. In this example, we use two print statement 
        to wrap "func()"
        """
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

def say_whee():
    print("Whee!")
# Call [my_first_decorator] function taking [say_whee] as a argument.
say_whee = my_first_decorator(say_whee)

# In effect, the name say_whee now points to the wrapper() inner function. 
# Remember that you return wrapper as a function when you call my_decorator(say_whee)
say_whee

# + hidden=true
say_whee()

# + [markdown] hidden=true
# Put simply: **decorators wrap a function, modifying its behavior.**
#
# Because wrapper() is a regular Python function, **the way a decorator modifies a function can change dynamically.** So as not to disturb your neighbors, the following example will only run the decorated code during the day:

# + hidden=true
from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Hush, the neighbors are asleep
    return wrapper

def say_whee():
    print("Whee!")

say_whee = not_during_the_night(say_whee)
print(say_whee)

# + hidden=true
say_whee()


# + [markdown] heading_collapsed=true hidden=true
# ## Revisting code using decorator syntactic sugar

# + hidden=true
def my_first_decorator(func):
    def wrapper():
        """
        Define a inner wrapper function wraping an argument functor 
        with pre-defined behaviors. In this example, we use two print statement 
        to wrap "func()"
        """
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

"""
So, @my_first_decorator is just an easier way of saying: 
  say_whee_decor = my_first_decorator(say_whee_decor)

It’s how you apply a decorator to a function.
"""
@my_first_decorator
def say_whee_decor():
    print("Whee!")

def say_whee():
    print("Whee!")
    
print(say_whee_decor)
print(say_whee)


# + [markdown] heading_collapsed=true
# # Decorator Syntax Alternatives

# + [markdown] heading_collapsed=true hidden=true
# ## Reusing Decorators defined in an own module
# Defining decorators in a module file and import them for easy reusability are available.

# + [markdown] hidden=true
# Let’s move the decorator to its own module that can be used in many other functions.
#
# Create a file called decorators.py with the following content:

# + hidden=true
def do_twice(func):
    """
    You can name your inner function whatever you want, and a generic name like wrapper() 
    is usually okay.
    """
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice


# + hidden=true
from decorators import do_twice

@do_twice
def say_whee():
    print("Whee!")


# + [markdown] heading_collapsed=true hidden=true
# ## Decorating Functions With Arguments
#
# Say that you have a function that accepts some arguments. Can you still decorate it?

# + hidden=true
def do_twice(func):
    """
    Taking [func] without any arguments
    """
    def wrapper():
        func()
        func()
    return wrapper

@do_twice
def greet(name):
    """
    Define a function decorated by [do_twice]
    """
    print(f"Hello {name}")


# + hidden=true
"""
The problem is that the inner function wrapper() does not take any arguments, 
but name="World" was passed to it. You could fix this by letting wrapper_do_twice() 
accept one argument, but then it would not work for the say_whee() function you 
created earlier.
"""
greet("World")


# + [markdown] hidden=true
# The solution is to use **\*args** and **\*\*kwargs** in the inner wrapper function. Then it will accept an arbitrary number of positional and keyword arguments. 
# - **\*args**: 
# - **\*\*kwargs**: 

# + hidden=true
def do_twice(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper

@do_twice
def greet(name):
    print(f"Hello {name}")


# + hidden=true
print(greet())
print(greet("Bing"))


# + [markdown] hidden=true
# ## Returning Values From Decorated Functions
# What happens to the return value of decorated functions? Well, that’s up to the decorator to decide.

# + hidden=true
def do_twice(func):
    def wrapper(*args, **kwargs):
        """
        Using a return statement to return an object from the calling of "func"
        We need to make sure the wrapper function returns the return value of 
        the decorated function. 
        """
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper

@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"

hi_adam = return_greeting("Adam")
print(hi_adam)

# + [markdown] hidden=true
# ## Multiple decorators
#
# ```
# @dec2
# @dec1
# def func(arg1, arg2, ...):
#     pass
# ```
# This is equivalent to:
# ```
# def func(arg1, arg2, ...):
#     pass
# func = dec2(dec1(func))
# ```
#
# -

# # A Few Real World Examples
#
# Let’s look at a few more useful examples of decorators. You’ll notice that they’ll mainly follow the same pattern that you’ve learned so far.

# +
# This formula is a good boilerplate template for building more complex decorators.
import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something before
        value = func(*args, **kwargs)
        # Do something after
        return value
    return wrapper_decorator


# -

# ## Define functions to be executed at entry or exist

# +
def on_exit(f):
    import atexit
    atexit.register(f)
    return f

@on_exit
def func():
    pass


# -

# ## Timing Functions
#
# Let’s start by creating a @timer decorator. It will [measure the time a function takes to execute](https://realpython.com/python-timer/) and print the duration to the console. Here’s the code:

# +
import functools
import time

def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()    # 1
        value = func(*args, **kwargs)
        end_time = time.perf_counter()      # 2
        run_time = end_time - start_time    # 3
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value
    return wrapper_timer

@timer
def waste_some_time(num_times):
    for _ in range(num_times):
        sum([i**2 for i in range(10000)])

waste_some_time(100)
# -

# **Note:** The @timer decorator is great if you just want to get an idea about the runtime of your functions. If you want to do more precise measurements of code, you should instead consider the [timeit module](https://docs.python.org/3/library/timeit.html) in the standard library. It temporarily disables garbage collection and runs multiple trials to strip out noise from quick function calls.

# ## Debugging Code
# The following @debug decorator will print the arguments a function is called with as well as its return value every time the function is called:

# +
import functools

def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           # 4
        return value
    return wrapper_debug


# -

# The signature is created by joining the string representations of all the arguments. The numbers in the following list correspond to the numbered comments in the code:
# 1. Create a list of the positional arguments. Use repr() to get a nice string representing each argument.
# 2. Create a list of the keyword arguments. The f-string formats each argument as key=value where the !r specifier means that repr() is used to represent the value.
# 3. The lists of positional and keyword arguments is joined together to one signature string with each argument separated by a comma.
# 4. The return value is printed after the function is executed.
#
# Let’s see how the decorator works in practice by applying it to a simple function with one position and one keyword argument:

@debug
def make_greeting(name, age=None):
    if age is None:
        return f"Howdy {name}!"
    else:
        return f"Whoa {name}! {age} already, you are growing up!"
make_greeting("Benjamin")

make_greeting("Richard", age=112)

make_greeting(name="Dorrisile", age=116)

# This example also shows how you can apply a decorator to a function that has already been defined. The approximation of e is based on the following series expansion:
# \begin{equation}
# e = \sum_{n=0}^{}\frac{1}{n!} = \frac{1}{0!} + \frac{1}{1!} + \frac{1}{2!} + \ldots
# \end{equation}

# +
import math
# from decorators import debug

# Apply a decorator to a standard library function
math.factorial = debug(math.factorial)

def approximate_e(terms=18):
    return sum(1 / math.factorial(n) for n in range(terms))


# -

# When calling the approximate_e() function, you can see the @debug decorator at work:
approximate_e(5)

# ## Slowing Down Code
# This next example might not seem very useful. Why would you want to slow down your Python code? Probably the most common use case is that you want to rate-limit a function that continuously checks whether a resource—like a web page—has changed. The @slow_down decorator will sleep one second before it calls the decorated function:

# +
import functools
import time

def slow_down(func):
    """Sleep 1 second before calling the function"""
    @functools.wraps(func)
    def wrapper_slow_down(*args, **kwargs):
        time.sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_down

@slow_down
def countdown(from_number):
    if from_number < 1:
        print("Liftoff!")
    else:
        print(from_number)
        countdown(from_number - 1)
countdown(3)
# -

# ## Registering Plugins
# Decorators don’t have to wrap the function they’re decorating. They can also simply register that a function exists and return it unwrapped. This can be used, for instance, to create a light-weight plug-in architecture:

# +
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)
PLUGINS
# -

randomly_greet("Alice")

# Using the @register decorator, you can create your own curated list of interesting variables, effectively hand-picking some functions from globals().









# + [markdown] heading_collapsed=true
# ## Define a class with a singleton instance.
#
# Note that once the class disappears enterprising programmers would have to be more creative to create more instances.

# + hidden=true
def singleton(cls):
    instances = {}
    def getinstance():
        if cls not in instances:
            instances[cls] = cls()
        return instances[cls]
    return getinstance

@singleton
class MyClass:
    ...


# + [markdown] heading_collapsed=true
# ## Add attributes to a function

# + hidden=true
def attrs(**kwds):
    def decorate(f):
        for k in kwds:
            setattr(f, k, kwds[k])
        return f
    return decorate

@attrs(versionadded="2.2",
       author="<NAME>")
def mymethod(f):
    ...


# + [markdown] heading_collapsed=true
# ## Enforce function argument and return types

# + hidden=true
def accepts(*types):
    def check_accepts(f):
        assert len(types) == f.func_code.co_argcount
        def new_f(*args, **kwds):
            for (a, t) in zip(args, types):
                assert isinstance(a, t), \
                       "arg %r does not match %s" % (a,t)
            return f(*args, **kwds)
        new_f.func_name = f.func_name
        return new_f
    return check_accepts

def returns(rtype):
    def check_returns(f):
        def new_f(*args, **kwds):
            result = f(*args, **kwds)
            assert isinstance(result, rtype), \
                   "return value %r does not match %s" % (result,rtype)
            return result
        new_f.func_name = f.func_name
        return new_f
    return check_returns

@accepts(int, (int,float))
@returns((int,float))
def func(arg1, arg2):
    return arg1 * arg2


# -

# ## Declare that a class implements a particular (set of) interface(s). 

# +
def provides(*interfaces):
     """
     An actual, working, implementation of provides for
     the current implementation of PyProtocols.  Not
     particularly important for the PEP text.
     """
     def provides(typ):
         declareImplementation(typ, instancesProvide=interfaces)
         return typ
     return provides

class IBar(Interface):
     """Declare something about IBar here"""

@provides(IBar)
class Foo(object):
        """Implement something here..."""
# -

# # Fancy Decorators
#
# So far, you’ve seen how to create simple decorators. You already have a pretty good understanding of what decorators are and how they work. Feel free to take a break from this article to practice everything you’ve learned.

# ## Decorating Classes
#
# There are two different ways you can use decorators on classes:
# 1. decorate the methods of a class using [@classmethod, @staticmethod](https://realpython.com/instance-class-and-static-methods-demystified/) and [@property](https://docs.python.org/3/library/functions.html#property)
# 2. decorate the whole class

#



# ## Nesting Decorators
# You can apply several decorators to a function by stacking them on top of each other

# +
from decorators import debug, do_twice

@debug
@do_twice
def greet(name):
    print(f"Hello {name}")
# -

# Think about this as the decorators being executed in the order they are listed. In other words, @debug calls @do_twice, which calls greet(), or debug(do_twice(greet())):

# ## Decorators with arguments





# ## Decorators that can optionally take arguments



# ## Stateful decorators





# ## Classes as decorators



# # Reference
#
# 1. [Primer on Python Decorators](https://realpython.com/primer-on-python-decorators/)
# 2. [PEP 318 – Decorators for Functions and Methods](https://peps.python.org/pep-0318/)
# 3. [The Decorator Pattern in Depth](https://blogs.oracle.com/javamagazine/post/the-decorator-pattern-in-depth)
# 4. [A Gentle Introduction to Decorators in Python](https://machinelearningmastery.com/a-gentle-introduction-to-decorators-in-python/)



StarcoderdataJupyterScriptsDedupFiltered