Решение задания 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.