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:
В 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 той самий:
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!