Python · Синтаксис · Продвинутый уровень
Декораторы
Функции, оборачивающие другие функции для изменения или расширения их поведения. Охватывает синтаксис `@`, декораторы с аргументами, стекирование и `functools.wraps`.
Краткое введение в тему и пояснения перед упражнениями (упражнения ниже):
Фабрики декораторов, стекирование, декораторы на основе класса
#Реальные паттерны, ошибки, когда не использовать, анонс декораторов методов
#Упражнения:
Декоратор timer
#Напишите декоратор `timer`, который измеряет время выполнения декорированной функции и печатает результат в формате: `'greet took 0.0001s'`. Используйте `time.time()` для измерения.
import time
def timer(func):
pass
@timer
def greet(name):
return f'Привет, {name}!'
greet('Алиса') # печатает: greet took 0.0001s (примерно)
Решение
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f'{func.__name__} took {elapsed:.4f}s')
return result
return wrapper
@timer
def greet(name):
return f'Привет, {name}!'
greet('Алиса')
Декоратор log_calls
#Напишите декоратор `log_calls`, который печатает имя функции, её позиционные аргументы и возвращаемое значение каждый раз при вызове. Формат: `'add called with (2, 3) -> 5'`.
def log_calls(func):
pass
@log_calls
def add(a, b):
return a + b
add(2, 3) # печатает: add called with (2, 3) -> 5
add(10, 20) # печатает: add called with (10, 20) -> 30
Решение
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f'{func.__name__} called with {args} -> {result}')
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(2, 3)
add(10, 20)
Декоратор memoize
#Напишите декоратор `memoize`, который кэширует результаты функции. При повторном вызове с теми же аргументами — вернуть кэшированное значение без повторного вызова функции. Хранить результаты в словаре с ключом `args`.
def memoize(func):
pass
@memoize
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
print(fib(10)) # 55
print(fib(30)) # 832040 (быстро благодаря кэшу)
Решение
import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
print(fib(10)) # 55
print(fib(30)) # 832040
Декоратор count_calls
#Напишите декоратор `count_calls`, который отслеживает количество вызовов декорированной функции. Храните счётчик как атрибут `.count` на wrapper, чтобы код мог его читать.
def count_calls(func):
pass
@count_calls
def say_hello():
print('Привет!')
say_hello()
say_hello()
say_hello()
print(say_hello.count) # 3
Решение
import functools
def count_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
wrapper.count += 1
return func(*args, **kwargs)
wrapper.count = 0
return wrapper
@count_calls
def say_hello():
print('Привет!')
say_hello()
say_hello()
say_hello()
print(say_hello.count) # 3
Декоратор validate_positive
#Напишите декоратор `validate_positive`, который проверяет каждый позиционный аргумент перед вызовом функции. Если любой аргумент не является положительным числом (≤ 0), выбросьте `ValueError` с сообщением `'All arguments must be positive'`.
def validate_positive(func):
pass
@validate_positive
def area(width, height):
return width * height
print(area(3, 4)) # 12
print(area(-1, 5)) # ValueError: All arguments must be positive
Решение
import functools
def validate_positive(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if any(a <= 0 for a in args):
raise ValueError('All arguments must be positive')
return func(*args, **kwargs)
return wrapper
@validate_positive
def area(width, height):
return width * height
print(area(3, 4)) # 12
try:
area(-1, 5)
except ValueError as e:
print(e) # All arguments must be positive
Декоратор once
#Напишите декоратор `once`, который гарантирует, что декорированная функция выполнится только один раз. При первом вызове — выполните функцию и сохраните результат. При всех последующих вызовах — возвращайте сохранённый результат без повторного выполнения.
def once(func):
pass
@once
def load_config():
print('Загрузка конфигурации...')
return {'debug': True}
c1 = load_config() # печатает: Загрузка конфигурации...
c2 = load_config() # ничего не печатает
c3 = load_config() # ничего не печатает
print(c1 is c2) # True
Решение
import functools
def once(func):
sentinel = object()
result = sentinel
@functools.wraps(func)
def wrapper(*args, **kwargs):
nonlocal result
if result is sentinel:
result = func(*args, **kwargs)
return result
return wrapper
@once
def load_config():
print('Загрузка конфигурации...')
return {'debug': True}
c1 = load_config()
c2 = load_config()
print(c1 is c2) # True
Фабрика декораторов retry
#Напишите фабрику декораторов `retry(n)`, которая повторяет выполнение декорированной функции до `n` раз при возникновении любого исключения. Если все попытки провалились — перебросьте последнее исключение. Печатайте `'Attempt X failed'` после каждой неудачной попытки.
def retry(n):
pass
attempts = [0]
@retry(3)
def flaky():
attempts[0] += 1
if attempts[0] < 3:
raise RuntimeError('not ready')
return 'success'
print(flaky()) # Attempt 1 failed, Attempt 2 failed, затем: 'success'
Решение
import functools
def retry(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, n + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == n:
raise
print(f'Attempt {attempt} failed')
return wrapper
return decorator
attempts = [0]
@retry(3)
def flaky():
attempts[0] += 1
if attempts[0] < 3:
raise RuntimeError('not ready')
return 'success'
print(flaky())
@property: класс Temperature
#Создайте класс `Temperature`, который хранит температуру в Цельсиях внутренне. Используйте `@property` для свойств `celsius` (геттер и сеттер) и `fahrenheit` (геттер и сеттер, конвертирующий из Фаренгейта в Цельсии). Формула: `F = C * 9/5 + 32`.
class Temperature:
def __init__(self, celsius=0):
pass
# Добавьте свойство celsius (геттер + сеттер)
# Добавьте свойство fahrenheit (геттер + сеттер)
t = Temperature(100)
print(t.celsius) # 100
print(t.fahrenheit) # 212.0
t.fahrenheit = 32
print(t.celsius) # 0.0
Решение
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5 / 9
t = Temperature(100)
print(t.celsius) # 100
print(t.fahrenheit) # 212.0
t.fahrenheit = 32
print(t.celsius) # 0.0
Фабрика декораторов requires_role
#Напишите фабрику декораторов `requires_role(role)`, которая защищает функцию. Декорированная функция всегда получает словарь `user` первым аргументом. Если `user['role']` не равен `role`, выбросьте `PermissionError` с сообщением `'Access denied'`. Иначе — вызовите функцию нормально.
def requires_role(role):
pass
@requires_role('admin')
def delete_record(user, record_id):
return f"Запись {record_id} удалена пользователем {user['name']}"
admin = {'name': 'Алиса', 'role': 'admin'}
guest = {'name': 'Боб', 'role': 'guest'}
print(delete_record(admin, 42)) # Запись 42 удалена пользователем Алиса
delete_record(guest, 42) # PermissionError: Access denied
Решение
import functools
def requires_role(role):
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if user.get('role') != role:
raise PermissionError('Access denied')
return func(user, *args, **kwargs)
return wrapper
return decorator
@requires_role('admin')
def delete_record(user, record_id):
return f"Запись {record_id} удалена пользователем {user['name']}"
admin = {'name': 'Алиса', 'role': 'admin'}
guest = {'name': 'Боб', 'role': 'guest'}
print(delete_record(admin, 42))
try:
delete_record(guest, 42)
except PermissionError as e:
print(e) # Access denied
Стекирование декораторов
#Примените декораторы `timer` и `log_calls` к функции `compute(n)`, которая возвращает сумму квадратов от 1 до n. Примените `@timer` снаружи и `@log_calls` внутри. Убедитесь, что вывод содержит и строку лога, и строку таймера.
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f'{func.__name__} took {time.time() - start:.4f}s')
return result
return wrapper
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f'{func.__name__} called with {args} -> {result}')
return result
return wrapper
# Примените оба декоратора к compute:
def compute(n):
return sum(i * i for i in range(1, n + 1))
compute(5)
Решение
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f'{func.__name__} took {time.time() - start:.4f}s')
return result
return wrapper
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f'{func.__name__} called with {args} -> {result}')
return result
return wrapper
@timer
@log_calls
def compute(n):
return sum(i * i for i in range(1, n + 1))
compute(5)
# log_calls печатает: compute called with (5,) -> 55
# timer печатает: compute took 0.0000s