⚖️ Старый и современный подход

⚡ Различия в двух словах

Старый: абстрактные классы через ручное наследование от abc.ABCMeta, исключения без контекста, код без аннотаций типов.

Новый: ABC и @abstractmethod, пользовательские исключения с полями, type hints, f-strings и guard clauses.

Вывод: современный код понятнее, безопаснее и проще в поддержке.

📜 Как показано в исходном конспекте

from abc import ABC, abstractmethod

class Employee(ABC):
    @abstractmethod
    def work(self):
        pass

class Programmer(Employee):
    def work(self):
        print("Write code")

p = Programmer()
p.work()

class AccessDeniedError(Exception):
    pass

def check_age(age):
    if age < 18:
        raise AccessDeniedError("Access denied: age must be at least 18")
    print("Access granted")

Что здесь происходит: код рабочий, но не содержит аннотаций типов, docstring и не передаёт дополнительный контекст в исключения.

❌ Почему этот подход может быть не лучшим сейчас

  • Без аннотаций типов сложнее понять, какие аргументы ожидает метод.
  • Без docstring IDE и help() не покажут назначение класса.
  • Исключение без дополнительных полей затрудняет логирование и отладку.
  • Дублирование проверок в наследниках увеличивает риск ошибки.

✅ Рекомендуемый современный вариант

from abc import ABC, abstractmethod


class Employee(ABC):
    """Abstract base class for company employees."""

    def __init__(self, name: str) -> None:
        self.name = name

    @abstractmethod
    def work(self) -> None:
        """Describe the employee's daily work."""
        pass


class AccessDeniedError(Exception):
    """Raised when a user is too young."""

    def __init__(self, age: int, min_age: int = 18) -> None:
        self.age = age
        self.min_age = min_age
        super().__init__(f"Access denied: age {age} is below {min_age}")


def check_age(age: int) -> None:
    if age < 18:
        raise AccessDeniedError(age)
    print("Access granted")

Что улучшилось:

  • Аннотации типов делают интерфейс очевидным.
  • Docstring помогает другим разработчикам и инструментам.
  • Исключение несёт контекст (age, min_age).
  • f-strings делают сообщения компактнее и читаемее.

🛡️ Guard clauses вместо вложенных if

# Старый стиль: вложенность
class PaypalPayment(PaymentProcessor):
    def pay(self, amount):
        if amount > 0:
            print(f"Paid {amount} via PayPal.")
        else:
            raise InvalidPaymentError("Amount must be positive")

# Современный стиль: guard clause
class PaypalPayment(PaymentProcessor):
    def pay(self, amount: float) -> None:
        if amount <= 0:
            raise InvalidPaymentError("Amount must be positive")
        print(f"Paid {amount} via PayPal.")

Guard clause (охранное условие) сразу выбрасывает исключение при нарушении контракта, а основной код остаётся без лишней вложенности.

🕰️ Когда старый подход ещё можно встретить

Код без type hints и f-strings встречается в legacy-проектах на Python 2.x или ранних версиях Python 3. При миграции стоит:

  • Постепенно добавлять аннотации типов.
  • Заменять "%s" % value и "{}".format(value) на f-strings.
  • Выделять повторяющуюся валидацию в методы или функции.
Проверить по документации: рекомендации по стилю в PEP 8; аннотации типов — PEP 484.