💻 Практические примеры

⚡ Минимальный рабочий пример

class Door:
    def __init__(self, code: str) -> None:
        self.__code = code

    def unlock(self, code: str) -> bool:
        if self.__is_valid_code(code):
            print("Access granted.")
            return True
        print("Access denied.")
        return False

    def __is_valid_code(self, code: str) -> bool:
        return code == self.__code

door = Door("1234")
door.unlock("0000")  # Access denied.
door.unlock("1234")  # Access granted.

Пример 1. Базовый класс Door

Простейшая версия: храним код в приватном атрибуте и проверяем его через метод unlock.

class Door:
    def __init__(self, code: str) -> None:
        self.__code = code

    def unlock(self, code: str) -> bool:
        if self.__is_valid_code(code):
            print("Access granted.")
            return True
        print("Access denied.")
        return False

    def __is_valid_code(self, code: str) -> bool:
        return code == self.__code


door = Door("1234")
door.unlock("0000")
door.unlock("1234")

Что происходит: метод __is_valid_code инкапсулирует правило проверки. unlock только принимает решение о доступе и возвращает bool.

Пример 2. Смена кода

Добавляем change_code. Новый код устанавливается только после проверки старого.

class Door:
    def __init__(self, code: str) -> None:
        self.__code = code

    def unlock(self, code: str) -> bool:
        if self.__is_valid_code(code):
            print("Access granted.")
            return True
        print("Access denied.")
        return False

    def change_code(self, old_code: str, new_code: str) -> None:
        if not self.__is_valid_code(old_code):
            print("Access denied. Code not changed.")
            return
        self.__code = new_code
        print("Code changed.")

    def __is_valid_code(self, code: str) -> bool:
        return code == self.__code


door = Door("1234")
door.change_code("0000", "9999")
door.unlock("1234")
door.change_code("1234", "9999")
door.unlock("1234")
door.unlock("9999")

Guard clause: сначала обрабатываем ошибочный сценарий и выходим, а дальше идёт «счастливый путь» без лишнего else.

Пример 3. Блокировка после неудачных попыток

Теперь дверь запоминает количество неудач и блокируется на заданное время. Время блокировки в примере уменьшено, чтобы можно было запустить код без долгого ожидания.

import time
from datetime import datetime, timedelta


class Door:
    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 = None

    def unlock(self, code: str) -> bool:
        if self.__is_blocked():
            self.__show_block_time()
            return False

        if self.__is_valid_code(code):
            print("Access granted.")
            self.__reset_attempts()
            return True

        print("Access denied.")
        self.__handle_failed_attempt()
        return False

    def change_code(self, old_code: str, new_code: str) -> None:
        if self.__is_blocked():
            self.__show_block_time()
            return

        if self.__is_valid_code(old_code):
            self.__code = new_code
            self.__reset_attempts()
            print("Code changed.")
            return

        print("Access denied. Code not changed.")
        self.__handle_failed_attempt()

    def __is_valid_code(self, code: str) -> bool:
        return code == self.__code

    def __is_blocked(self) -> bool:
        if self.__block_until is None:
            return False
        return datetime.now() < self.__block_until

    def __show_block_time(self) -> None:
        remaining = self.__block_until - datetime.now()
        seconds = int(remaining.total_seconds())
        minutes, sec = divmod(seconds, 60)
        print(f"Door is blocked. Try again in {minutes} min {sec} sec.")

    def __check_block(self) -> None:
        if self.__failed_attempts >= self.__max_attempts:
            self.__block_until = datetime.now() + self.__block_duration
            print("Too many failed attempts. ", end="")
            self.__show_block_time()

    def __handle_failed_attempt(self) -> None:
        self.__failed_attempts += 1
        self.__check_block()

    def __reset_attempts(self) -> None:
        self.__failed_attempts = 0


door = Door("1234", max_attempts=2, block_minutes=0.05)
door.unlock("1111")
door.change_code("2222", "9999")
door.unlock("1234")
time.sleep(5)
door.unlock("1234")

Важно: при успешном открытии или смене кода счётчик неудач сбрасывается. Иначе одна ошибка в прошлом могла бы блокировать дверь навсегда.

Пример 4. Пользовательское исключение

Заблокированная дверь теперь бросает DoorBlockedError, а вызывающий код сам решает, что с ним делать.

import time
from datetime import datetime, timedelta


class DoorBlockedError(Exception):
    '''Raised when trying to access a blocked door.'''
    pass


class Door:
    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 = None

    def unlock(self, code: str) -> bool:
        if self.__is_blocked():
            raise DoorBlockedError(self.__remaining_block_time())

        if self.__is_valid_code(code):
            print("Access granted.")
            self.__reset_attempts()
            return True

        print("Access denied.")
        self.__handle_failed_attempt()
        return False

    def change_code(self, old_code: str, new_code: str) -> None:
        if self.__is_blocked():
            raise DoorBlockedError(self.__remaining_block_time())

        if self.__is_valid_code(old_code):
            self.__code = new_code
            self.__reset_attempts()
            print("Code changed.")
            return

        print("Access denied. Code not changed.")
        self.__handle_failed_attempt()

    def __is_valid_code(self, code: str) -> bool:
        return code == self.__code

    def __is_blocked(self) -> bool:
        if self.__block_until is None:
            return False
        return datetime.now() < self.__block_until

    def __remaining_block_time(self) -> str:
        remaining = self.__block_until - datetime.now()
        seconds = int(remaining.total_seconds())
        minutes, sec = divmod(seconds, 60)
        return f"Door is blocked. Try again in {minutes} min {sec} sec."

    def __check_block(self) -> None:
        if self.__failed_attempts >= self.__max_attempts:
            self.__block_until = datetime.now() + self.__block_duration
            print("Too many failed attempts.")

    def __handle_failed_attempt(self) -> None:
        self.__failed_attempts += 1
        self.__check_block()

    def __reset_attempts(self) -> None:
        self.__failed_attempts = 0


door = Door("1234", max_attempts=2, block_minutes=0.05)

try:
    door.unlock("1111")
except DoorBlockedError as e:
    print(e)

try:
    door.change_code("0000", "9999")
except DoorBlockedError as e:
    print(e)

try:
    door.unlock("1234")
except DoorBlockedError as e:
    print(e)

time.sleep(5)

try:
    door.unlock("1234")
except DoorBlockedError as e:
    print(e)

Зачем исключение: оно передаёт управление вызывающему коду. Вместо бездумного print можно залогировать событие, показать пользователю диалог или отправить уведомление охране.

Проверить по документации: подробнее о пользовательских исключениях см. User-defined Exceptions, о f-strings — Formatted string literals.