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()

Вивід:

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()

Вивід той самий:

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

Декоратори з параметрами:

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

Doc-string та ім'я декоратора:

Щоб уникнути отримання імені та doc-string декоратора замість оригінальної функції, використовуйте декоратор functools.wraps. Functools wraps оновить декоратор атрибутами декорованої функції:

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

Зверніть увагу: використання functools.wraps є гарною практикою.

Гарний приклад декоратора:

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}!"

МНОЖИННІ ДЕКОРАТОРИ!!

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)}!"


#Як це працює? like_numbers йде до @arrow, потім все йде до @round_dict, потім @number_filter
#Коротко:
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)

Вимога прав адміністратора (декоратори) з виключеннями:

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!