<jupyter_start><jupyter_text>Decorators (Part 1) Recall the example in the last section where we wrote a simple closure to count how many times a function had been run:<jupyter_code>def counter(fn):
    count = 0
    
    def inner(*args, **kwargs):
        nonlocal count
        count += 1
        print(f'Function {fn.__name__} was called {count} times')
        return fn(*args, **kwargs)
    return inner
def add(a, b=0):
    """
    returns the sum of a and b
    """
    return a + b
help(add)<jupyter_output>Help on function add in module __main__:

add(a, b=0)
    returns the sum of a and b<jupyter_text>Here's the memory address that `add` points to:<jupyter_code>id(add)<jupyter_output><empty_output><jupyter_text>Now we create a closure using the `add` function as an argument to the `counter` function:<jupyter_code>add = counter(add)<jupyter_output><empty_output><jupyter_text>And you'll note that `add` is no longer the same function as before. Indeed the memory address `add` points to is no longer the same:<jupyter_code>id(add)
add(1, 2)
add(2, 2)<jupyter_output>Function add was called 2 times<jupyter_text>What happened is that we put our **add** function 'through' the **counter** function - we usually say that we **decorated** our function **add**.And we call that **counter** function a **decorator**.There is a shorthand way of decorating our function without having to type:``func = counter(func)``<jupyter_code>@counter
def mult(a: float, b: float=1, c: float=1) -> float:
    """
    returns the product of a, b, and c
    """
    return a * b * c

# mult = counter(mult)
mult(1, 2, 3)
mult(2, 2, 2)<jupyter_output>Function mult was called 2 times<jupyter_text>Let's do a little bit of introspection on our two decorated functions:<jupyter_code>add.__name__
def f():
    pass

f.__name__
mult.__name__<jupyter_output><empty_output><jupyter_text>As you can see, the name of the function is no longer **add** or **mult**, but instead it is the name of that **inner** function in our decorator.<jupyter_code>help(add)
help(mult)<jupyter_output>Help on function inner in module __main__:

inner(*args, **kwargs)<jupyter_text>As you can see, we've also lost our docstring and parameter annotations! What about introspecting the parameters of **add** and **mult**:<jupyter_code>import inspect
inspect.getsource(add)
inspect.getsource(mult)<jupyter_output><empty_output><jupyter_text>Even the signature is gone:<jupyter_code>inspect.signature(add)
inspect.signature(mult)<jupyter_output><empty_output><jupyter_text>Even the parameter defaults documentation is are gone:<jupyter_code>inspect.signature(add).parameters<jupyter_output><empty_output><jupyter_text>In general, when we create decorated functions, we end up "losing" a lot of the metadata of our original function! However, we **can** put that information back in - it can get quite complicated.Let's see how we might be able to do that for some simple things, like the docstring and the function name.<jupyter_code>def counter(fn):
    count = 0
    
    def inner(*args, **kwargs):
        nonlocal count
        count += 1
        print("{0} was called {1} times".format(fn.__name__, count))
    inner.__name__ = fn.__name__
    inner.__doc__ = fn.__doc__
    return inner
@counter
def add(a: int, b: int=10) -> int:
    """
    returns sum of two integers
    """
    return a + b
help(add)
add.__name__<jupyter_output><empty_output><jupyter_text>At least we have the docstring and function name back... But what about the parameters? Our real **add** function takes two positional parameters, but because the closure used a generic way of accepting **\*args** and **\*\*kwargs**, we lose this information We can use a special function in the **functools** module, called **wraps**. In fact, that function is a decorator itself!<jupyter_code>from functools import wraps
def counter(fn):
    count = 0
    
    @wraps(fn)
    def inner(*args, **kwargs):
        nonlocal count
        count += 1
        print("{0} was called {1} times".format(fn.__name__, count))

    return inner
@counter
def add(a: int, b: int=10) -> int:
    """
    returns sum of two integers
    """
    return a + b
help(add)<jupyter_output>Help on function add in module __main__:

add(a: int, b: int = 10) -> int
    returns sum of two integers<jupyter_text>Yay!!! Everything is back to normal.<jupyter_code>inspect.getsource(add)
inspect.signature(add)
inspect.signature(add).parameters
def outer(fn):
    def inner(*args, **kwargs):
        print('a simple decorator')
        return fn(*args, **kwargs)
    return inner

def my_function():
    '''doc string for my_function'''
    pass

my_function.__name__
help(my_function)
@outer
def my_function():
    '''doc string for my_function'''
    pass
my_function.__name__
help(my_function)
from functools import wraps
def outer(fn):
    @wraps(fn)
    def inner(*args, **kwargs):
        print('a simple decorator')
        return fn(*args, **kwargs)
    return inner
@outer
def my_function():
    '''doc string for my_function'''
    pass
my_function.__name__<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11052322163433274, 'fraction_numerical': 0.006466784244562022, 'mean_word_length': 4.395348837209302, 'pattern_counts': {'":': 0, '<': 48, '<?xml version=': 0, '>': 52, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '13', 'dir': 'jupyter-structured-clean-dedup', 'id': '205885', 'license': 'MIT', 'n_tokens_mistral': 1645, 'n_tokens_neox': 1601, 'n_words': 565, 'path': 'Functional Programming/Scopes, Closures and Decorators/06 - Decorators - Part 1.ipynb', 'repo_name': 'Rafiei-tayebe/AP4002', 'size': '5103'}