Decorators

Декоратори - то є функцією з замиканням, яка приймає як аргумент функцію, всередині має визначення функції та повертає функцію. Наприклад:

def decorator(func):  
    def inner():  
        print("Before decorating function")  
        func()  
        print("After decorating function")  

    return inner  

def printer():  
    print("Hello, world, decorators!") 


decorated_function = decorator(printer)  
decorated_function()

Output:

Before decorating function
Hello, world, decorators!
After decorating function

В Python зробили синтаксичний цукор у наступному вигляді:

def decorator(func):  
    def inner(*args, **kwargs):  
        print("Before decorating function")  
        result = func(*args, **kwargs)  
        print("After decorating function")  
        return result

    return inner  

@decorator
def printer():  
    print("Hello, world, decorators!")  


printer()

Output той самий:

Before decorating function
Hello, world, decorators!
After decorating function

Decorators with parameters:

def calculate_profit(years):
    def decorate(func):
        def profit(amount, percentage):
            output = amount
            for _ in range(years):
                output *= (1 + percentage / 100)
            return func(output - amount)
        return profit
    return decorate


@calculate_profit(5)
def message_taxes(calculated_profit):
    print(f"Profit amount - {calculated_profit}")


print(message_taxes(1000, 5))  # Profit amount - 276.281

Decorators doc-string and name:

To avoid receiving the decorator's name, and doc-string, use the functools.wraps decorator. Functools wraps will update the decorator with the decorated function attributes:

from functools import wraps

def decorator_func(func):
    """Decorator doc-string"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
    return wrapper


@decorator_func
def some_func(arg):
    """Function doc-string"""
    pass

print(some_func.__name__)  # some_func
print(some_func.__doc__)  # Function doc-string

Please note: it is a good practice to use functools.wraps decorator.

Good decorator example:

import functools
import time


def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        function = func(*args, **kwargs)
        end_time = time.time()
        print(f"Function runtime: {round(end_time-start_time, 4)}s")
        return function
    return wrapper


@timer
def count_range(n):
    count = 0
    for number in range(n): 
         count += number
    return count


count_range(100)  # Function runtime: 0.0s
count_range(500)  # Function runtime: 0.0001s
count_range(30000)  # Function runtime: 0.0033s

from typing import Callable

def only_admin(func: Callable) -> Callable:
    def wrapper(users: list) -> None:
        admin_list = [user for user in users if user["is_admin"] is True]
        func(admin_list)
    return wrapper


@only_admin
def create_permissions(users: list) -> None:
    for user in users:
        print(f'Creating permissions for {user["username"]}')
from typing import Callable


def html_tag(tag: str) -> Callable:
    def inner(func: Callable) -> Callable:
        def wrapper(*args, **kwargs) -> str:
            return f"<{tag}>{func(*args, **kwargs)}</{tag}>"
        return wrapper
    return inner


@html_tag("div")
def greeter(name: str) -> str:
    return f"Hello, {name}!"

MULTIPLE DECORATORS!!

from typing import Callable

def number_filter(func: Callable) -> Callable:
    def wrapper(items: list) -> str:
        new_list = [number for number in items if isinstance(number, (int, float))]
        result = func(new_list)
        return result
return wrapper


def round_dict(func: Callable) -> Callable:
    def wrapper(items: list) -> str:
        new_dict = {round(number): round(number) * 2 for number in items}
        return func(new_dict)
    return wrapper


def arrow(func: Callable) -> Callable:
    def wrapper(items: dict) -> str:
        new_list = [f"{key} -> {number}" for key, number in items.items()]
        return func(new_list)
    return wrapper


@number_filter
@round_dict
@arrow
def like_numbers(items: list) -> str:
    return f"I like to filter, rounding, doubling, store and decorate numbers: {', '.join(items)}!"



#How it works? like_numbers goes to @arrow, then all goes to @round_dict, then @number_filter
#In a nutshell:
def resulting_function(items: list):
    list_with_only_ints = [number for number in items if isinstance(number, (int, float))]
    dict_with_numbers_and_doubled_numbers = {round(number): round(number) * 2 for number in list_with_only_ints}
    result_list_with_arrows = [f"{key} -> {number}" for key, number in dict_with_numbers_and_doubled_numbers.items()]
    return like_numbers(result_list_with_arrows)

Requiring admin (decorators) with exceptions:

import functools  
from typing import Callable  


class UnauthenticatedError(Exception):  
    pass  

class PermissionDeniedError(Exception):  
    pass  

def login_required(func: Callable) -> Callable:  
    @functools.wraps(func)  
    def inner(request: dict, *args, **kwargs) -> UnauthenticatedError | Callable:  
        if "user" not in request:  
            raise UnauthenticatedError(  
                "Authentication credentials were not provided!"  
            )  

        return func(request, *args, **kwargs)  

    return inner  


def admin_required(func: Callable) -> Callable:  
    @functools.wraps(func)  
    def inner(request: dict, *args, **kwargs) -> PermissionDeniedError | Callable:  
        if not request["user"]["is_admin"]:  
            raise PermissionDeniedError("User must be admin!")  

        return func(request, *args, **kwargs)  

    return inner  


@login_required  
@admin_required  
def access_admin_page(request: dict) -> None:  
    print(f"Welcome to the admin page, {request['user']['full_name']}")  





# request = {"user": {"full_name": "James Bond", "is_admin": True}}  
# access_admin_page(request)  
# # "Welcome to the admin page, James Bond"  
#  
request = {"user": {"full_name": "John Smith", "is_admin": False}}  
access_admin_page(request)  
# PermissionDeniedError: User must be admin!  

# request = {}  
# access_admin_page(request)  
# # UnauthenticatedError: Authentication credentials were not provided!