Что делает метод __init__ в классе и когда он вызывается
Что проверяют
Базовое ООП на Python у человека, который каждый день пишет классы датасетов, моделей и трансформеров. Вопрос звучит просто, но по ответу видно, понимаете ли вы, что объект создаётся не в __init__, и знаете ли соглашения, на которых держатся библиотеки вроде sklearn и PyTorch.
Как отвечать
__init__ вызывается автоматически сразу после создания экземпляра и задаёт его начальное состояние. Первым аргументом приходит уже существующий объект, поэтому корректнее называть метод инициализатором: сам объект создаёт __new__, а __init__ только наполняет его атрибутами. Вернуть из него что-то, кроме None, нельзя.
class TabularDataset:
def __init__(self, path, transform=None):
self.path = path
self.transform = transformВ ML-коде поверх этого лежат свои правила:
- у эстиматоров sklearn в
__init__гиперпараметры только сохраняются как есть, без валидации и вычислений: на этом держатсяget_params,cloneи перебор в поиске по сетке, а вся работа идёт вfit; - в наследнике
nn.Moduleпервым делом зовутsuper().__init__(), иначе присвоенные слои и параметры не зарегистрируются; - тяжёлое чтение данных в инициализаторе это осознанное решение: то, что прочитано там, попадает в каждый воркер загрузчика.
Где спотыкаются
Изменяемое значение по умолчанию: def __init__(self, features=[]) создаёт список один раз при определении функции, и он становится общим для всех экземпляров. Пишут None, а настоящее значение создают внутри тела.
Забывают вызвать инициализатор родителя при наследовании. В обычном классе это иногда сходит с рук, в nn.Module или в наследнике от BaseEstimator ломает регистрацию состояния, причём не сразу, а на сохранении или на переносе модели.
Путают атрибут класса и атрибут экземпляра. Объявленный в теле класса список или счётчик живёт один на всех объектах, и накопление в него из разных экземпляров даёт результат, который потом долго ищут в логах обучения.