✅ Решения заданий

⚠️ Рекомендация: попробуйте решить задания самостоятельно, прежде чем смотреть ответы. Решения — это последняя инстанция, когда собственные попытки исчерпаны.

⚡ Кратко

  • Задание 1: фиксированный лимит 20 символов в замыкании.
  • Задание 2: параметризованный декоратор limit_output(max_len).
  • Задание 3: кэш на обычном словаре.
  • Задание 4: кэш на OrderedDict с вытеснением popitem(last=False).
  • Задание 5: логирование через logging.info в файл.
  • Задание 6: проверка через zip(args, expected_types) и isinstance.

Решение 1. Ограничение длины строки

def limit_output(func):
    max_len = 20

    def wrapper() -> str:
        result = func()
        if len(result) > max_len:
            return result[:max_len - 3] + "..."
        return result

    return wrapper


@limit_output
def get_text() -> str:
    return "Это очень длинный текст, который нужно обрезать."


print(get_text())  # Это очень длинный...

Логика: декоратор фиксирует лимит в замыкании. Обёртка проверяет длину результата и, если нужно, обрезает строку, оставляя место для троеточия.

Решение 2. Ограничение с указанием длины строки

def limit_output(max_len: int):
    def decorator(func):
        def wrapper() -> str:
            result = func()
            if len(result) > max_len:
                return result[:max_len - 3] + "..."
            return result
        return wrapper
    return decorator


@limit_output(26)
def get_text() -> str:
    return "Это очень длинный текст, который нужно обрезать."


print(get_text())  # Это очень длинный текст...

Логика: внешняя функция limit_output получает параметр, средняя — декорируемую функцию, внутренняя — выполняет обрезку.

Решение 3. Кеширование результатов

def cache(func):
    memo: dict[tuple, int] = {}

    def wrapper(*args):
        if args not in memo:
            memo[args] = func(*args)
        else:
            print("Результат из кеша: ")
        return memo[args]

    return wrapper


@cache
def multiply(a: int, b: int) -> int:
    print(f"Вычисляем {a} * {b}: ")
    return a * b


print(multiply(2, 3))
print(multiply(2, 3))
print(multiply(4, 5))
print(multiply(2, 3))

Логика: ключом кэша служит кортеж позиционных аргументов. Если ключ уже есть, печатается сообщение и возвращается сохранённое значение.

Решение 4. Кеш с ограничением по размеру

from collections import OrderedDict


def cache(max_size: int = 5):
    def decorator(func):
        memo: OrderedDict[tuple, int] = OrderedDict()

        def wrapper(*args):
            if args in memo:
                print("Результат из кеша: ")
                memo.move_to_end(args)
            else:
                result = func(*args)
                if len(memo) >= max_size:
                    memo.popitem(last=False)
                memo[args] = result
            return memo[args]

        return wrapper
    return decorator


@cache(max_size=2)
def multiply(a: int, b: int) -> int:
    print(f"Вычисляем {a} * {b}: ")
    return a * b


print(multiply(2, 3))
print(multiply(2, 3))
print(multiply(4, 5))
print(multiply(6, 7))
print(multiply(2, 3))  # будет пересчитан

Логика: OrderedDict хранит порядок вставки. При повторном доступе элемент переносится в конец через move_to_end. Если кэш переполнен, удаляется самый старый элемент через popitem(last=False).

Решение 5. Журнал вызовов функции

import logging

logging.basicConfig(
    filename="call_log.log",
    level=logging.INFO,
    format="%(message)s",
    encoding="utf-8",
)


def log_to_file(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        all_args = ", ".join(str(arg) for arg in args)
        all_kwargs = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
        logging.info(
            f"function: {func.__name__} | "
            f"args: {all_args if all_args else None}; "
            f"kwargs: {all_kwargs if all_kwargs else None} | "
            f"return: {result}"
        )
        return result
    return wrapper


@log_to_file
def add(a: int, b: int) -> int:
    return a + b


@log_to_file
def greet_with_name(name: str, punctuation: str = "!") -> str:
    return f"Hello, {name}{punctuation}"


@log_to_file
def greet() -> str:
    return "Hello"


add(5, 3)
greet_with_name("Alice", punctuation=".")
greet()

Логика: обёртка вызывает исходную функцию, формирует строку с аргументами и результатом и записывает её через logging.info. Аргументы None используются, когда позиционных или ключевых аргументов нет.

Решение 6. Проверка типов аргументов по порядку

def accepts_types(*expected_types):
    def decorator(func):
        def wrapper(*args):
            for arg, expected in zip(args, expected_types):
                if not isinstance(arg, expected):
                    raise TypeError(
                        f"Incorrect type for argument '{arg}': "
                        f"expected - {expected}, received - {type(arg)}"
                    )
            return func(*args)
        return wrapper
    return decorator


@accepts_types(int, int)
def add(a: int, b: int) -> int:
    return a + b


@accepts_types(str)
def greet(name: str) -> str:
    return f"Hello, {name}!"


@accepts_types(str, str)
def greet_by_name(name: str, punctuation: str = "!") -> str:
    return f"Hello, {name}{punctuation}"


try:
    print(add(5, 3))
    print(greet_by_name("Anna"))
    # print(add("one", 3))             # ошибка
    print(greet_by_name("Anna", 0))  # ошибка
except Exception as e:
    print(e)

Логика: zip(args, expected_types) сопоставляет каждый переданный аргумент с ожидаемым типом по порядку. При несовпадении выбрасывается TypeError с подробным сообщением.

Проверить по документации: для промышленной проверки типов используйте аннотации и статический анализатор mypy вместо ручных декораторов.