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

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

Старый: класс без подсказок типов, использование datetime.strptime, локальная переменная может затенять импорт date.

Новый: аннотации типов, явное именование переменных, @classmethod и @staticmethod как декораторы, f-strings, а также @dataclass как альтернатива для простых данных.

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

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

Исходное решение работает, но не содержит аннотаций типов. Обратите внимание, что внутри from_string переменная названа date и затеняет импортированный объект date из модуля datetime.

from datetime import datetime, date


class Student:
    current_id = 1

    def __init__(self, name, birth_date):
        self.birth_date = datetime.strptime(birth_date, "%Y-%m-%d").date()
        if not self.is_valid_birthdate():
            raise ValueError("Student must be at least 16 years old.")

        self.name = name
        self.student_id = Student.current_id
        Student.current_id += 1

    def __str__(self):
        return f"Student: {self.name}, birth_date: {self.birth_date}, ID: {self.student_id}"

    def __repr__(self):
        return f"Student: {self.name}, birth_date: {self.birth_date}, ID: {self.student_id}"

    def show_info(self):
        print(f"Student:\n\tName: {self.name}\n\tAge: {self.calculate_age()}\n\tID: {self.student_id}")

    def calculate_age(self):
        today = date.today()
        return self.calculate_age_on(today)

    def calculate_age_on(self, target_date):
        age = target_date.year - self.birth_date.year
        if (target_date.month, target_date.day) < (self.birth_date.month, self.birth_date.day):
            age -= 1
        return age

    def is_valid_birthdate(self):
        return self.calculate_age() >= 16

    @classmethod
    def from_string(cls, data):
        name, date = map(str.strip, data.split(", "))
        return cls(name, date)

    @staticmethod
    def filter_by_min_age(students, min_age):
        return [s for s in students if s.calculate_age() >= min_age]

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

  • Без аннотаций типов IDE и линтеры не подскажут, какие аргументы ожидаются.
  • Имя локальной переменной date в from_string затеняет импорт date, что путает при чтении и может привести к ошибкам.
  • Минимальный возраст захардкожен числом 16; лучше вынести в константу класса.
  • __str__ и __repr__ в исходном материале совпадают, хотя семантически они разные.

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

Современный Python позволяет добавить типы, убрать затенение имён и сделать различие между строковыми представлениями явным.

from datetime import datetime, date


class Student:
    current_id: int = 1
    MIN_AGE: int = 16

    def __init__(self, name: str, birth_date: str) -> None:
        self.birth_date = datetime.strptime(birth_date, "%Y-%m-%d").date()
        if not self.is_valid_birthdate():
            raise ValueError("Student must be at least 16 years old.")

        self.name = name
        self.student_id = Student.current_id
        Student.current_id += 1

    def __str__(self) -> str:
        return f"Student: {self.name}, birth_date: {self.birth_date}, ID: {self.student_id}"

    def __repr__(self) -> str:
        return (
            f"Student(name={self.name!r}, birth_date={self.birth_date!r}, "
            f"student_id={self.student_id!r})"
        )

    def show_info(self) -> None:
        print(
            f"Student:\n\tName: {self.name}\n\tAge: {self.calculate_age()}\n\tID: {self.student_id}"
        )

    def calculate_age(self) -> int:
        return self.calculate_age_on(date.today())

    def calculate_age_on(self, target_date: date) -> int:
        age = target_date.year - self.birth_date.year
        if (target_date.month, target_date.day) < (self.birth_date.month, self.birth_date.day):
            age -= 1
        return age

    def is_valid_birthdate(self) -> bool:
        return self.calculate_age() >= self.MIN_AGE

    @classmethod
    def from_string(cls, data: str) -> "Student":
        name, date_string = map(str.strip, data.split(","))
        return cls(name, date_string)

    @staticmethod
    def filter_by_min_age(students: list["Student"], min_age: int) -> list["Student"]:
        return [s for s in students if s.calculate_age() >= min_age]

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

  • Аннотации типов делают интерфейс класса понятным без чтения тела методов.
  • Константа MIN_AGE вынесена из кода и легко изменяется.
  • Переменная переименована в date_string, чтобы не затенять date.
  • __repr__ возвращает строку, пригодную для воссоздания объекта.

Альтернатива: @dataclass

Если бы класс был «контейнером данных» без сложной валидации, можно было бы использовать @dataclass. Но здесь нужна проверка возраста, поэтому __init__ всё равно придётся писать явно через __post_init__.

from dataclasses import dataclass, field
from datetime import datetime, date


@dataclass
class Student:
    name: str
    birth_date: str
    student_id: int = field(init=False)
    current_id: ClassVar[int] = 1
    MIN_AGE: ClassVar[int] = 16

    def __post_init__(self) -> None:
        self.birth_date = datetime.strptime(self.birth_date, "%Y-%m-%d").date()
        if not self.is_valid_birthdate():
            raise ValueError("Student must be at least 16 years old.")

        self.student_id = Student.current_id
        Student.current_id += 1

    def calculate_age(self) -> int:
        today = date.today()
        age = today.year - self.birth_date.year
        if (today.month, today.day) < (self.birth_date.month, self.birth_date.day):
            age -= 1
        return age

    def is_valid_birthdate(self) -> bool:
        return self.calculate_age() >= self.MIN_AGE

В учебном материале @dataclass не используется, поэтому для выполнения заданий применяйте обычный класс.

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

В legacy-коде и старых учебных материалах классы часто пишут без аннотаций. Главное — понимать логику: datetime.strptime парсит строку, @classmethod получает cls, @staticmethod не получает self. При рефакторинге добавляйте типы постепенно и убирайте затенение имён.

Проверить по документации: @dataclass и ClassVar описаны в dataclasses и typing.ClassVar. Аннотации типов — в typing и PEP 484.