Наследование, super() и переопределение методов
Содержание курса
Антипаттерн: наследование ради данных вместо атрибута
Разобравшись с тем, как наследование работает в правильных случаях, стоит посмотреть на типичную ошибку — когда оно применяется не потому, что есть реальное is-a отношение, а просто чтобы «прикрепить» данные к объекту.
Классический пример — администратор в системе пользователей. Допустим, нужно добавить к пользователю роль. Соблазн выглядит так:
class AdminUser(str):
pass
admin = AdminUser("alice")
print(admin) # alice
print(type(admin)) # <class '__main__.AdminUser'>
Здесь AdminUser наследуется от встроенного str — видимо, чтобы сам объект нёс строковое значение (имя или роль). Технически это работает. Но подставим фразу: «AdminUser является строкой» — и сразу становится неловко. AdminUser — это пользователь, а не строка. Строка — это тип данных, которым описывается роль или имя. Связь между AdminUser и str — не is-a, а has-a: пользователь имеет имя, имеет роль.
Правильное решение — обычный класс с атрибутами:
class User:
def __init__(self, name: str, role: str = "user") -> None:
self.name = name
self.role = role
class AdminUser(User):
def __init__(self, name: str) -> None:
super().__init__(name, role="admin")
Теперь AdminUser является разновидностью User — это честное is-a отношение. Роль хранится в атрибуте, а не зашита в тип через наследование от str.
Почему исходный вариант хуже на практике:
- Поменять роль у объекта
AdminUser(str)нельзя —strиммутабелен, значение фиксируется при создании. В варианте с атрибутом это одна строчка:user.role = "moderator". isinstance(admin, str)возвращаетTrue— объект ведёт себя как строка везде, где ожидается строка. Это создаёт неожиданные побочные эффекты: функции, принимающиеstr, молча примутAdminUser, хотя логически это разные вещи.- Добавить новые данные или поведение к
AdminUser(str)значительно сложнее:str.__init__не принимает произвольные параметры, и расширение черезsuper()там нетривиально.
Один вопрос помогает сразу поставить диагноз: «Я наследуюсь от X, потому что мой класс является X, или потому что мне нужно хранить X?» Если ответ второй — нужен атрибут, а не родительский класс.
Этот антипаттерн чаще всего возникает при работе со встроенными типами: list, dict, str. Иногда наследование от них действительно оправдано — например, class SortedList(list) расширяет список специфическим поведением вставки, и объект действительно является списком. Но если единственная причина — хранить одно значение определённого типа, атрибут всегда чище и безопаснее.
