# ---
# 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
# ---

# ## Decoradores ¿Qué son?

# ### Funciones que añaden funcionalidades a otras funciones.
#
# Por eso se las denomina "decoradores", porque "decoran" a otras funciones. Les añaden funcionalidades.
#
# ### Estructura de un decorador:
#
# - 3 funciones (A, B, C) donde A recibe como parámetro a B para devolver C.
# - Un decorador devuelve una función

# ```
# def función_decorador(función):
#     def función_interna():
#         # código de la función interna
#     return función_interna
# ```

# Ejemplo base

# +
def suma():
    print(15+20)

def resta():
    print(30-10)
    
suma()
resta()


# +
def suma():
    print('vamos a realizar un cálculo')
    print(15+20)
    print('hemos terminado el cálculo')

def resta():
    print('vamos a realizar un cálculo')
    print(30-10)
    print('hemos terminado el cálculo')
    
suma()
resta()


# -

# Lo mismo pero empleando un decorador

# Ahora se mejorara, pensar que pueden ser 100, lo que se requiere es añadir una nueva funcionalidad a todas las funciones. Sol.: Agregar una función decoradora y aplicar a la que se quiere.
#
# Nececidad: Imprimir un mensaje antes y después de realizar el cálculo. 

# +
def funcion_decoradora(funcion_parametro):
    def funcion_interior():
        print("vamos a realizar un cálculo")
        funcion_parametro()
        print("hemos terminado el cálculo")
    return funcion_interior

@funcion_decoradora
def suma():
    print(15+20)

@funcion_decoradora
def resta():
    print(30-10)
    

suma()
resta()


# -

# ### Decoradores con parámetros

# El ejemplo anterior sólo se limita a imprimir mensajes; ahora se implementara para que las funciones reciban parámetros. 

# +
def funcion_decoradora(funcion_parametro):
    def funcion_interior(*args):
        print("vamos a realizar un cálculo")
        funcion_parametro(*args)
        print("hemos terminado el cálculo")
    return funcion_interior

@funcion_decoradora
def suma(num1, num2):
    print(num1+num2)

@funcion_decoradora
def resta(num1, num2):
    print(num1-num2)
    

suma(2,3)
resta(3,2)


# -

# #### Ahora utilizando parámetros keyword

# +
def funcion_decoradora(funcion_parametro):
    def funcion_interior(*args, **kwords):
        print("vamos a realizar un cálculo")
        funcion_parametro(*args, **kwords)
        print("hemos terminado el cálculo")
    return funcion_interior

@funcion_decoradora
def suma(num1, num2):
    print(num1+num2)

@funcion_decoradora
def resta(num1, num2):
    print(num1-num2)

@funcion_decoradora
def potencia(base, exponente):
    print(pow(base, exponente))
    
suma(2,3)
resta(3,2)
potencia(base=2, exponente=3)


# -

# Y ahora logrando que las funciones devuelvan resultados...

# +
def funcion_decoradora(funcion_parametro):
    def funcion_interior(*args, **kwords):
        print("vamos a realizar un cálculo")
        salida = funcion_parametro(*args, **kwords)
        print("hemos terminado el cálculo")
        return salida
    return funcion_interior

@funcion_decoradora
def suma(num1, num2):
    return num1+num2

@funcion_decoradora
def resta(num1, num2):
    return num1-num2

@funcion_decoradora
def potencia(base, exponente):
    return pow(base, exponente)
    
print(suma(2,3))
print(resta(3,2))
print(potencia(base=2, exponente=3))


# -

# #### Agregando argumentos al decorador

# +
def funcion_decoradora(is_valid = True):
    def _funcion_decoradora(funcion_parametro):
        def before_action():
            print("vamos a realizar un cálculo")
            
        def after_action():
            print("hemos terminado el cálculo")
        
        def funcion_interior(*args, **kwords):
            if is_valid:
                before_action()
            salida = funcion_parametro(*args, **kwords)
            after_action()
            return salida
        return funcion_interior
    return _funcion_decoradora

@funcion_decoradora(is_valid=False)
def suma(num1, num2):
    return num1+num2

@funcion_decoradora()
def resta(num1, num2):
    return num1-num2

@funcion_decoradora()
def potencia(base, exponente):
    return pow(base, exponente)
    
print(suma(2,3))
print(resta(3,2))
print(potencia(base=2, exponente=3))


# -

# ### Otros ejemplos

# +
def todo_mayuscula(f):
    def mayuscula():
        return f().upper()
    return mayuscula

@todo_mayuscula
def holamundo():
    return 'Hola, mundo!'

hola = holamundo()
print(hola)


# -

# #### Etiquetas html

# +
def tag_html(func):
    def interna(*args, **kwargs):
        return '<html>' + func(*args, **kwargs) + '</html>'
    return interna

@tag_html
def hola():
    return "Dentro de función"

print(hola())
# -

# #### Validando parámetros

# Validación de enteros

# +
from functools import wraps

def validate_type(type):
    def validate(func):
        @wraps(func)
        def inner(*args):
            if all(isinstance(val, type) for val in args):
                return func(*args)
        return inner
    return validate

@validate_type(int)
def suma(x, y):
    return x + y

print(suma(1, 1))
print(suma.__name__)


# -

# Validación con parámetros

# +
def validate_type(type):
    def validate(func):
        def inner(*args, **kwargs):
            if all(isinstance(val, type) for val in args):
                return func(*args)
        return inner
    return validate

@validate_type(str)
def suma_entera(x, y):
    return x + y

print(suma_entera('a', 'a'))

# -

# Logs
#

# +
def log(func):
    def inner(*args):
        print(func.__name__, f'args:{args}')
        return func(*args)
    return inner

@log
def suma(x, y):
    return x + y

print(suma(2, 3))
# -

# Uso de esto en flask http://flask.palletsprojects.com/en/1.1.x/quickstart/#quickstart

# Obteniendo nombres de funciones

# +
from functools import wraps

def validate_type(type):
    def validate(func):
        @wraps(func)
        def inner(*args, **kwargs):
            if all(isinstance(val, type) for val in args):
                return func(*args)
        return inner
    return validate

@validate_type(int)
def suma_entera(x, y):
    return x + y

print(suma_entera.__name__)
print(suma_entera)

# +
from functools import lru_cache

@lru_cache()
def fib(n):
    #print(n)
    if n < 2:
        return n
    return fib(n-2) + fib(n-1)

print(fib(6))
# -

# Otra función importante...

# +
from functools import singledispatch
from collections.abc import MutableSequence

@singledispatch
def generar_html(x):
    return x

@generar_html.register(int)
def _generar_con_int(x):
    return f'<span>{x}</span>'

@generar_html.register(MutableSequence)
def _generar_con_list(x):
    return f'<p>{x}</p>'

@generar_html.register(str)
def _generar_con_str(x):
    return f'<body>{x}</body>'

generar_html('Hola')


# -

# #### Controlando el tiempo de ejecución

# +
def calc_square(numbers):
    result = []
    for number in numbers:
        result.append(number*number)
    return result

def calc_cube(numbers):
    result = []
    for number in numbers:
        result.append(number*number*number)
    return result

array = range(1, 100000)
out_square = calc_square(array)
out_cube = calc_cube(array)
#print(out_cube)
# -

# Requerimiento: Medir los tiempos que le toma a cada función terminar de procesar la lista de números. Reto: Reconocer que se puede mejorar en el código. 

# +
import time

def calc_square(numbers):
    start = time.time()
    result = []
    for number in numbers:
        result.append(number*number)
    end = time.time()
    print('calc_square took ' + str((end-start)*1000) + ' mil sec')
    return result

def calc_cube(numbers):
    start = time.time()
    result = []
    for number in numbers:
        result.append(number*number*number)
    end = time.time()
    print('calc_cube took ' + str((end-start)*1000) + ' mil sec')
    return result

array = range(1, 100000)
out_square = calc_square(array)
out_cube = calc_cube(array)
#print(out_cube)
# -



# Las funciones son objetos de primera clase en python. Lo que significa es que pueden tratarse como cualquier otra variable y puede pasarlos como argumento a otra función o incluso devolverlos como un valor de retorno.

# + jupyter={"outputs_hidden": true}
import time

def time_it(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__ + " took " + str((end-start)*1000) + " mil sec")
        return result
    return wrapper

@time_it
def calc_square(numbers):
    result = []
    for number in numbers:
        result.append(number*number)
    return result

@time_it
def calc_cube(numbers):
    result = []
    for number in numbers:
        result.append(number*number*number)
    return result

array = range(1, 100000)
out_square = calc_square(array)
out_cube = calc_cube(array)
# -

# ### Decorador para medir el tiempo de ejecución y escribir logs

# +
from functools import wraps
import time

def my_logger(orig_func):
    import logging
    logging.basicConfig(filename='{}.log'.format(orig_func.__name__), level=logging.INFO)
    
    @wraps(orig_func)
    def wrapper(*args, **kwargs):
        logging.info(f'Ran with args: {args}, and kwargs {kwargs}')
        return orig_func(*args, **kwargs)
    
    return wrapper

def my_timer(orig_func):
    
    @wraps(orig_func)
    def wrapper(*args, **kwargs):
        t1 = time.time()
        result = orig_func(*args, **kwargs)
        t2 = time.time() - t1
        
        print(f'{orig_func.__name__} ran in: {t2} sec.')
        return result
    
    return wrapper


@my_logger
@my_timer
def display_info(name, age):
    time.sleep(1)
    #print(f'display_info ran with arguments ({name}, {age})')


display_info('María', 23)

StarcoderdataJupyterScriptsDedupFiltered