📖 Теория: повторение перед практикумом

⚡ Кратко

Практикум строится на трёх идеях: инкапсуляции (скрываем внутреннее состояние), DRY (не дублируем проверку кода) и пользовательских исключениях (сообщаем о блокировке через raise).

  • Приватные атрибуты начинаются с двух подчёркиваний: self.__code.
  • Один приватный метод __is_valid_code используется и в unlock, и в change_code.
  • Блокировка — это состояние объекта, ограниченное по времени: self.__block_until.
  • Собственное исключение наследуется от Exception и называется с суффиксом Error.

1. Инкапсуляция и приватные атрибуты

Инкапсуляция — сокрытие внутреннего устройства объекта от внешнего кода. В Python приватность — соглашение, подкреплённое манглированием имён: атрибут self.__code превращается в self._Door__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 __is_valid_code(self, code: str) -> bool:
        return code == self.__code

Почему именно так: код доступа — внутреннее состояние замка. Прямое изменение door.__code извне ломает логику безопасности. Приватный атрибут сигнализирует: «сюда не лезь».

2. DRY: не дублируй проверку

Методы unlock и change_code оба проверяют код. Вынесем проверку в приватный метод __is_valid_code.

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

Если правила проверки изменятся (например, добавится хеширование), правку придётся сделать в одном месте.

3. Блокировка через datetime

Для временной блокировки нужно запомнить момент, до которого дверь недоступна. Текущее время — datetime.now(), сдвиг — timedelta(minutes=...).

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

Проверка «заблокирована ли дверь» — это сравнение datetime.now() < self.__block_until.

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

Когда встроенных ошибок недостаточно, создаём свой класс-исключение. В практикуме это DoorBlockedError.

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

try:
    door.unlock("1234")
except DoorBlockedError as e:
    print("Дверь заблокирована:", e)

Наследование от Exception позволяет отлавливать ошибку отдельно от других.

5. Современные практики

Решения в этом уроке дополнены аннотациями типов, f-strings и guard clauses — проверками, которые возвращают управление раньше, уменьшая вложенность.

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

Сначала обрабатываем исключительную ситуацию (блокировка), потом — штатный сценарий. Так код читается сверху вниз без лишних else.

Проверить по документации: о соглашениях по стилю см. PEP 8, об аннотациях типов — PEP 484, о docstring — PEP 257.