Links: PYTHON - PROGRAMMING
Rel: python functions; property - @property is a decorator
Ref:
- https://stackoverflow.com/questions/308999/what-does-functools-wraps-do
Tags: #public


from functools import wraps


decorators

e.g. flask routes @app.route wraps the view function so that when '/route/' is called, your template is rendered

@app.route('/about')
def about():
    return render_template('about.html')

def manip()

>>> from functools import wraps
>>> def manip(func):
...     @wraps(func)
...     def inner(*args, **kwargs):
...             func()
...             print('This also executes')
...             return 
...     return inner
... 
>>> def hello():
...     print('hello world')
... 
>>> hello()
hello world
>>> 
>>> @manip
... def hello():
...     print('hello world')
... 
>>> hello()
hello world
This also executes
>>> 

syntactic sugar
metaprogramming because code modifying code at run-time

functions are objects too (names with bound attributes)

higher-order functions - take functions as arguments

def execute_this(func, *args):
    return func(*args)


>>> execute_this(print, 'Hello World')
'Hello World'

A decorator is a high-order function that (wraps and) returns another function:

from functools import wraps

def add_two(func): #expects a function argument
    @wraps(func) # passes metadata through wrapper {: id="passes-metadata-through-wrapper" }
    def inner(*args, **kwargs):
        r = func(*args, **kwargs)
        return r + 2
    return inner #returns the wrapper (w/ func.__name__, __module__, __doc__)

def prod_five():
    return 5


prod_seven = add_two(prod_five) # new function, same as: {: id="new-function,-same-as:" }

@add_two
def prod_five():
    return 5        

wrappers call top-bottom and execute function -> bottom -> top

def mod_seven(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) % 7
    return wrapper

@mod_seven
@add_two
def prod_five():
    return 5

# -> mod_seven(add_two(prod_five))() {: id="->-mod-seven(add-two(prod-five))()" }
# -> outputs: 0 {: id="->-outputs:-0" }

@add_two
@mod_seven
def prod_five():
    return 5

# -> add_two(mod_seven(prod_five))() {: id="->-add-two(mod-seven(prod-five))()" }
# -> outputs: 5  {: id="->-outputs:-5" }



more complex example:

def wrap_in_x(func):
    def inner(*args, **kwargs):
        print("x" * 30)
        func(*args, **kwargs)
        print("x" * 30)
    return inner


def wrap_in_y(func):
    def inner(*args, **kwargs):
        print("y" * 30)
        func(*args, **kwargs)
        print("y" * 30)
    return inner


@wrap_in_x
@wrap_in_y
def xy_wrap(msg):
    print(msg)


>>> xy_wrap("Hello World!w")

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
Hello World!
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

so calling xy_wrap() calls wrap_in_x -> wrap_in_y -> xy_wrap -> return

wrap_in_x -> print("x" * 30) -> func(*args, **kwargs), where func is wrap_in_y ->
wrap_in_y -> print("y" * 30) -> func(*args, **kwargs), where func is xy_wraps ->
xy_wraps -> print("Hello World!") -> xy_wraps has now returned (competed all statements), so
wrap_in_y -> print("y" * 30) -> wrap_in_y now has returned, so
wrap_in_x -> print("x" * 30) -> returned


decorate methods simply by taking self into account

def add_two(mtd):
    def wrapper(self, *args, **kwargs):
        return mtd() + 2
    return wrapper

use decorators for debugging

import time
from functools import wraps

def timeit(func):
    """Decorator to time a function """
    @wraps(func)
    def time_wrapper(*args, **kwargs):
        start = time.time()
        function = func(*args, **kwargs)
        end = time.time()
        total = end - start

        print(f'func:{func.__name__} args:{[args, kwargs]} took: {total} s')
        return function
    return time_wrapper
from functools import partial, wraps

def debug(func=None, *, prefix=''):
    """ """
    if func is None:
        return partial(debug, prefix=prefix)

    msg = prefix + func.__qualname__
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(msg)
        return func(*args, **kwargs)
    return wrapper

etc

def rename(newname):
    """ a simple generic decorator to dynamically re-name a function """
    def dec(f):
        f.__name__ = newname
        return f
    return dec

References:
- https://gist.github.com/Zearin/2f40b7b9cfc51132851a
- https://realpython.com/primer-on-python-decorators
- https://www.programiz.com/python-programming/decorator#decorating
-