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

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

Решение задания 1. Онлайн-платёжные системы

from abc import ABC, abstractmethod


class PaymentProcessor(ABC):
    """Abstract base class for payment processors."""

    @abstractmethod
    def pay(self, amount: float) -> None:
        """Process a payment of the given amount."""
        pass


class PaypalPayment(PaymentProcessor):
    def pay(self, amount: float) -> None:
        print(f"Paid {amount} via PayPal.")


class CreditCardPayment(PaymentProcessor):
    def pay(self, amount: float) -> None:
        print(f"Paid {amount} via Credit Card.")


# Пример использования
payment1 = PaypalPayment()
payment2 = CreditCardPayment()

payment1.pay(100)
payment2.pay(200)

Логика решения: абстрактный класс задаёт контракт, а наследники реализуют конкретное поведение.

Решение задания 2. Проверка платежей

from abc import ABC, abstractmethod


class InvalidPaymentError(Exception):
    """Raised when the payment amount is invalid."""
    pass


class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, amount: float) -> None:
        pass

    @staticmethod
    def _validate_amount(amount: float) -> None:
        """Guard clause for positive amounts."""
        if amount <= 0:
            raise InvalidPaymentError("Payment amount must be positive.")


class PaypalPayment(PaymentProcessor):
    def pay(self, amount: float) -> None:
        self._validate_amount(amount)
        print(f"Paid {amount} via PayPal.")


class CreditCardPayment(PaymentProcessor):
    def pay(self, amount: float) -> None:
        self._validate_amount(amount)
        print(f"Paid {amount} via Credit Card.")


# Пример использования
payments = [PaypalPayment(), CreditCardPayment()]

for payment in payments:
    try:
        payment.pay(100)
        payment.pay(-50)  # Ошибка
    except InvalidPaymentError as e:
        print("Error:", e)

Логика решения: общая проверка вынесена в статический метод базового класса, чтобы не дублировать код.

Решение задания 3. Фигуры и площади

import math
from abc import ABC, abstractmethod


class Shape(ABC):
    """Abstract base class for geometric shapes."""

    @abstractmethod
    def area(self) -> float:
        """Return the area of the shape."""
        pass


class Circle(Shape):
    def __init__(self, radius: float) -> None:
        self.radius = radius

    def area(self) -> float:
        return math.pi * self.radius ** 2


class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height


shapes = [Circle(3), Rectangle(4, 5)]
for shape in shapes:
    print(f"Area: {shape.area():.2f}")

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

Решение задания 4. Проверка размеров фигур

import math
from abc import ABC, abstractmethod


class InvalidSizeError(ValueError):
    """Raised when a shape dimension is not positive."""
    pass


class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass


class Circle(Shape):
    def __init__(self, radius: float) -> None:
        if radius <= 0:
            raise InvalidSizeError(f"Radius must be positive, got {radius}")
        self.radius = radius

    def area(self) -> float:
        return math.pi * self.radius ** 2


class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        if width <= 0 or height <= 0:
            raise InvalidSizeError(f"Width and height must be positive, got {width}x{height}")
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height


# Проверка
for invalid in [lambda: Circle(-1), lambda: Rectangle(0, 5)]:
    try:
        invalid()
    except InvalidSizeError as e:
        print("Caught InvalidSizeError:", e)

Логика решения: валидация происходит в конструкторе — объект не может существовать в некорректном состоянии. Наследование от ValueError позволяет ловить ошибку шире.

Решение задания 5 ★. Итератор диапазона

class Countdown:
    """Iterator counting down from start to 1."""

    def __init__(self, start: int) -> None:
        if start < 1:
            raise ValueError("Start must be a positive integer")
        self.start = start
        self.current = start

    def __iter__(self):
        return self

    def __next__(self) -> int:
        if self.current < 1:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value


for n in Countdown(5):
    print(n)  # 5 4 3 2 1

Логика решения: __iter__ возвращает сам объект, а __next__ уменьшает счётчик до тех пор, пока не достигнет нижней границы.

Решение задания 6 ★★. Умная дверь с блокировкой

from datetime import datetime, timedelta


class DoorBlockedError(Exception):
    """Дверь заблокирована после серии неудачных попыток."""


class Door:
    """
    Кодовый замок с блокировкой после неудачных попыток.

    Attributes:
        code (str): Текущий код доступа.
        failed_attempts (int): Счётчик неудачных попыток.
        max_attempts (int): Максимальное число попыток до блокировки.
        block_duration (timedelta): Длительность блокировки.
        block_until (datetime | None): Момент окончания блокировки.
    """

    def __init__(self, code: str, max_attempts: int = 3, block_minutes: float = 15.0) -> None:
        self.code = code
        self.failed_attempts = 0
        self.max_attempts = max_attempts
        self.block_duration = timedelta(minutes=block_minutes)
        self.block_until: datetime | None = None

    def is_valid_code(self, code: str) -> bool:
        """Возвращает True, если код совпадает с текущим."""
        return code == self.code

    def is_blocked(self) -> bool:
        """Возвращает True, пока время блокировки не истекло."""
        if self.block_until is None:
            return False
        if datetime.now() >= self.block_until:
            self.block_until = None
            self.failed_attempts = 0
            return False
        return True

    def remaining_block_time(self) -> timedelta:
        """Сколько осталось до конца блокировки."""
        if not self.is_blocked():
            return timedelta(0)
        return self.block_until - datetime.now()

    def check_block(self) -> None:
        """
        Проверяет блокировку перед операцией.

        Raises:
            DoorBlockedError: Если дверь ещё заблокирована.
        """
        if self.is_blocked():
            minutes = self.remaining_block_time().total_seconds() / 60
            raise DoorBlockedError(f"Дверь заблокирована ещё на {minutes:.1f} мин.")

    def reset_attempts(self) -> None:
        """Сбрасывает счётчик неудачных попыток."""
        self.failed_attempts = 0

    def handle_failed_attempt(self) -> None:
        """Учитывает неудачную попытку и при необходимости блокирует дверь."""
        self.failed_attempts += 1
        if self.failed_attempts >= self.max_attempts:
            self.block_until = datetime.now() + self.block_duration
            print("Too many failed attempts — дверь заблокирована")
        else:
            print(f"Access denied (попытка {self.failed_attempts} из {self.max_attempts})")

    def unlock(self, code: str) -> bool:
        """
        Пытается открыть дверь.

        Raises:
            DoorBlockedError: Если дверь заблокирована.
        """
        self.check_block()
        if self.is_valid_code(code):
            self.reset_attempts()
            print("Access granted")
            return True
        self.handle_failed_attempt()
        return False

    def change_code(self, old_code: str, new_code: str) -> bool:
        """
        Меняет код доступа при верном старом коде.

        Raises:
            DoorBlockedError: Если дверь заблокирована.
        """
        self.check_block()
        if not self.is_valid_code(old_code):
            self.handle_failed_attempt()
            print("Code not changed")
            return False
        self.code = new_code
        self.reset_attempts()
        print("Code changed")
        return True


door = Door("1234", max_attempts=3, block_minutes=15)

door.unlock("0000")   # Access denied (попытка 1 из 3)
door.unlock("1111")   # Access denied (попытка 2 из 3)
door.unlock("2222")   # Too many failed attempts — дверь заблокирована

try:
    door.unlock("1234")           # даже правильный код не поможет
except DoorBlockedError as error:
    print(error)                  # Дверь заблокирована ещё на 15.0 мин.

Логика решения:

  • check_block() — единственная точка проверки блокировки, её вызывают и unlock(), и change_code(). Благодаря этому правило «сначала проверь блокировку» не дублируется.
  • is_blocked() не только отвечает на вопрос, но и снимает истёкшую блокировку, обнуляя счётчик — поэтому после окончания срока дверь работает как новая.
  • handle_failed_attempt() собирает в одном месте счётчик и решение о блокировке: при достижении max_attempts вычисляется block_until.
  • Успешная операция всегда вызывает reset_attempts() — иначе старые неудачи накопятся и заблокируют дверь у того, кто ввёл верный код.
  • Собственное исключение DoorBlockedError отделяет «дверь заблокирована» от «код неверный»: первое — исключительная ситуация, второе — обычный отказ, возвращающий False.