Магический метод __init__
Инициализация новых объектов, передача им начальных параметров и понимание роли аргумента self.
Введение в жизненный цикл объектов: От чертежа к реальности
Добро пожаловать в двадцать восьмой урок нашего курса. До этого момента мы рассматривали классы как некие абстрактные структуры, шаблоны или чертежи. Мы говорили о том, что класс определяет общее поведение, но настоящая магия объектно-ориентированного программирования начинается в тот момент, когда абстрактный чертеж превращается в конкретный, осязаемый объект в памяти компьютера. Этот процесс называется инстанцированием или созданием экземпляра класса.
Представьте себе завод по производству автомобилей. Чертеж автомобиля — это наш класс. На чертеже указано, что у машины должны быть колеса, двигатель, цвет кузова и VIN-номер. Однако вы не можете сесть в чертеж и поехать на нем на работу. Вам нужен реальный автомобиль, сошедший с конвейера. Каждый такой автомобиль будет иметь свой уникальный цвет, свой конкретный двигатель и свой уникальный номер, хотя все они созданы по одному и тому же чертежу. В программировании этот первый этап сборки, когда объекту присваиваются его уникальные характеристики (состояние), называется инициализацией. И в языке Python за этот важнейший этап отвечает специальный метод, который называется __init__.
Метод __init__ относится к категории так называемых 'магических' методов (magic methods или dunder methods, от слов double underscore — двойное подчеркивание). Эти методы вызываются интерпретатором Python автоматически при наступлении определенных событий. В случае с __init__ событием является создание нового экземпляра класса. Важно понимать тонкую, но критическую разницу: __init__ не создает объект физически (за выделение памяти отвечает другой метод — __new__), он лишь инициализирует уже созданный объект, наполняя его стартовыми данными. Понимание этой механики является водоразделом между начинающим программистом и разработчиком уровня Intermediate. В этом уроке мы разберем метод __init__ на атомы, изучим все нюансы передачи аргументов, поймем философский и технический смысл загадочного параметра self и научимся избегать критических ошибок, таких как использование изменяемых объектов в качестве аргументов по умолчанию.
Анатомия метода __init__ и роль аргумента self
Давайте заглянем под капот и разберем синтаксис и логику работы метода инициализации. Когда вы пишете def __init__(self): внутри определения класса, вы создаете функцию, которая привязывается к будущему объекту. Самое первое, что бросается в глаза любому новичку, пришедшему в Python из других языков (например, Java, C++ или JavaScript), — это обязательное наличие параметра self в качестве первого аргумента. В других языках ссылка на текущий объект часто скрыта под ключевым словом this и передается неявно. Python же, следуя своей философии 'Явное лучше, чем неявное' (The Zen of Python), требует указывать этот параметр явно.
Что же такое self? Слово 'self' переводится как 'сам' или 'себя'. В контексте ООП self — это ссылка на конкретный экземпляр класса, с которым мы работаем в данный момент времени. Когда вы создаете два разных объекта одного класса, например, user1 и user2, класс у них один, методы в памяти хранятся в одном экземпляре. Как же метод понимает, чьи именно данные (какого из пользователей) ему нужно изменить или прочитать? Именно благодаря self. Когда вы вызываете user1.say_hello(), интерпретатор Python 'за кулисами' преобразует этот вызов в User.say_hello(user1). То есть сам объект передается в метод первым аргументом. Именно поэтому при определении метода в классе мы обязаны зарезервировать первое место для принимаемого объекта.
Внутри метода __init__ мы используем self для создания атрибутов экземпляра (instance attributes). Атрибуты — это переменные, которые принадлежат конкретному объекту. Написание self.name = 'Alice' означает: 'возьми текущий объект (self) и создай внутри него пространство имен, в котором переменная name будет указывать на строку Alice'. Если вы забудете использовать self. и просто напишете name = 'Alice' внутри __init__, вы создадите обычную локальную переменную, которая бесследно исчезнет из памяти сразу после завершения работы метода инициализации, и ваш объект останется пустым. Это одна из самых частых и обидных ошибок начинающих разработчиков.
class Product:
def __init__(self, name, price):
# self указывает на создаваемый объект
# Мы создаем атрибуты экземпляра
self.name = name
self.price = price
self.is_active = True # Значение по умолчанию для всех новых объектов
# Процесс инстанцирования
# 1. Python создает пустой объект в памяти
# 2. Вызывает __init__(self, 'Laptop', 1200)
# 3. self ссылается на этот новый объект
laptop = Product('Laptop', 1200)
print(f"Товар: {laptop.name}, Цена: {laptop.price}, Доступен: {laptop.is_active}")
Что в действительности делает метод __init__ в языке Python?
Что произойдет, если внутри метода __init__ создать переменную без использования префикса 'self.' (например, написать просто `age = 25`)?
Параметризация объектов: Передача аргументов в __init__
Мощь метода __init__ заключается в том, что он работает как любая другая функция в Python (за исключением того, что он не должен ничего возвращать с помощью return, кроме неявного None). Это означает, что мы можем применять к нему все те знания о функциях, которые мы получили в предыдущих модулях курса: позиционные аргументы, именованные аргументы, значения по умолчанию и даже распаковку списков и словарей через *args и **kwargs.
Рассмотрим сценарий, когда мы проектируем систему для интернет-магазина. У нас есть класс Customer (Покупатель). При регистрации покупателя нам обязательно нужно знать его email и пароль. Это обязательные параметры, без которых существование объекта покупателя лишено смысла. Однако такие данные, как номер телефона, адрес доставки или возраст, могут быть опциональными. Клиент может заполнить их позже в своем профиле. Как правильно спроектировать метод инициализации для такого класса? Мы используем позиционные или обязательные именованные аргументы для критически важных данных и задаем значения по умолчанию (обычно None, пустые строки или нули) для опциональных данных.
При проектировании параметров для __init__ старайтесь соблюдать баланс. Метод с двумя-тремя параметрами читается легко и понятно. Если же ваш метод принимает 10-15 параметров, это явный признак 'запаха кода' (code smell), указывающий на то, что ваш класс пытается делать слишком много вещей одновременно (нарушение принципа единственной ответственности - Single Responsibility Principle). В таких случаях стоит задуматься о декомпозиции: возможно, адрес доставки следует вынести в отдельный класс Address, а настройки профиля — в класс ProfileSettings. Тогда ваш основной класс Customer будет принимать в качестве параметров объекты этих вспомогательных классов. Это делает код более модульным, тестируемым и поддерживаемым. Помните принцип: простое лучше, чем сложное.
class Customer:
def __init__(self, username, email, phone=None, is_premium=False):
self.username = username
self.email = email
self.phone = phone
self.is_premium = is_premium
self.orders_count = 0 # Вычисляемый/начальный атрибут, не требующий передачи
# Примеры инициализации с разным набором аргументов:
# 1. Только обязательные позиционные аргументы
user1 = Customer('john_doe', 'john@example.com')
# 2. Использование именованных аргументов для читабельности
user2 = Customer(username='alice_w', email='alice@example.com', is_premium=True)
# 3. Передача всех аргументов, включая опциональный телефон
user3 = Customer('bob_builder', 'bob@example.com', '+1234567890')
Задание
Проанализируйте, как создаются объекты с разными наборами параметров. Попробуйте самостоятельно представить, как бы вы написали класс для банковского счета.
- Определите, какие данные являются обязательными (номер счета, владелец).
- Определите опциональные данные (начальный баланс, тип валюты по умолчанию).
- Создайте в голове структуру def __init__(self, ...).
- Определите атрибуты, которые создаются автоматически (например, пустой список транзакций).
Опасная ловушка: Изменяемые аргументы по умолчанию (Mutable Default Arguments)
В этом разделе мы затронем одну из самых коварных ловушек в Python, в которую попадают практически все Junior-разработчики (и даже некоторые Middle). Как мы обсуждали в материалах по функциям, объекты в Python делятся на изменяемые (mutable) и неизменяемые (immutable). К неизменяемым относятся числа, строки, кортежи. К изменяемым — списки, словари, множества. Проблема возникает, когда вы используете изменяемый объект в качестве значения по умолчанию для параметра в методе __init__. Эта ситуация часто всплывает на технических собеседованиях (Code Review) при переходе на уровень Intermediate.
Представьте, что вы пишете класс Student и хотите, чтобы у каждого нового студента по умолчанию был пустой список оценок. Вы пишете: def __init__(self, name, grades=[]): self.grades = grades. Кажется, всё логично. Вы создаете первого студента, добавляете ему оценку '5'. Затем создаете второго студента, проверяете его оценки и с ужасом видите, что у него уже стоит '5', хотя вы ему ничего не ставили! Как такое возможно? Дело в том, что значения по умолчанию для аргументов функции или метода вычисляются и создаются в памяти только ОДИН РАЗ — в момент определения класса (или функции), когда интерпретатор читает ваш файл с кодом. Этот единственный список [] сохраняется в метаданных функции. И теперь все объекты Student, которые создаются без явной передачи списка оценок, будут получать ссылку на один и тот же список в памяти. Изменяя его для одного студента, вы изменяете его для всех остальных!
Как правильно решать эту проблему? Существует золотой стандарт (Pythonic way): если вам нужно использовать изменяемый объект по умолчанию, используйте значение None в заголовке функции, а внутри тела __init__ делайте проверку. Например: if grades is None: self.grades = []. В этом случае при каждом создании объекта будет выполняться код тела метода, и для каждого объекта будет создаваться абсолютно новый, независимый пустой список. Это гарантирует изоляцию состояния объектов друг от друга, что является фундаментальным требованием объектно-ориентированного программирования.
# ❌ НЕПРАВИЛЬНЫЙ ПОДХОД (Ловушка изменяемого аргумента)
class StudentBad:
def __init__(self, name, grades=[]):
self.name = name
self.grades = grades
s1 = StudentBad('Иван')
s2 = StudentBad('Анна')
s1.grades.append(5) # Ставим оценку Ивану
print(s2.grades) # Выведет [5]! Анна неожиданно получила пятерку Ивана.
# ✅ ПРАВИЛЬНЫЙ ПОДХОД (Использование None)
class StudentGood:
def __init__(self, name, grades=None):
self.name = name
# Каждый раз создаем новый список, если ничего не передали
if grades is None:
self.grades = []
else:
self.grades = grades
student_a = StudentGood('Петр')
student_b = StudentGood('Мария')
student_a.grades.append(5)
print(student_b.grades) # Выведет []. Мария осталась без чужих оценок.
Какой из следующих вариантов объявления метода __init__ является наиболее безопасным и 'Pythonic' для инициализации атрибута-словаря?
Флеш-карточки
В какой момент вычисляются значения по умолчанию для аргументов метода __init__?
Нажмите, чтобы увидеть ответ
Один раз, во время компиляции файла интерпретатором (когда определяется класс), а не при каждом создании объекта.
Нажмите, чтобы вернуться
Какое ключевое слово используется для обозначения 'пустоты' в качестве безопасного значения по умолчанию для списков?
Нажмите, чтобы увидеть ответ
None. Внутри тела метода оно проверяется: if arg is None: self.arg = []
Нажмите, чтобы вернуться
Почему нельзя использовать изменяемые объекты (list, dict, set) в качестве значений по умолчанию?
Нажмите, чтобы увидеть ответ
Они создаются в памяти один раз. Все объекты, созданные без этого аргумента, будут разделять ссылку на один и тот же изменяемый объект, вызывая непредвиденные побочные эффекты.
Нажмите, чтобы вернуться
Валидация данных и инкапсуляция логики внутри __init__
Метод __init__ — это не просто глупый конвейер по перекладыванию переданных аргументов в атрибуты self. Это первая и главная линия обороны вашего класса. Это своего рода 'таможенный контроль', через который проходят данные, прежде чем стать частью состояния вашего объекта. В парадигме объектно-ориентированного программирования существует важное правило: объект никогда не должен находиться в неконсистентном (недействительном или противоречивом) состоянии. Если для создания объекта 'Автомобиль' требуется указать количество дверей, и кто-то передает значение '-1' или '100', класс должен немедленно заблокировать такую попытку, иначе дальнейшая логика программы будет разрушена.
Поэтому внутри __init__ крайне важно и нужно размещать логику валидации (проверки) данных. Вы можете проверять типы данных (с помощью isinstance), диапазоны значений, длину строк или соответствие формату с помощью регулярных выражений. Если переданные данные некорректны, лучшей практикой в Python является немедленное выбрасывание исключения (raise Exception), например, ValueError или TypeError. Это прерывает процесс создания объекта (объект не будет создан) и информирует программиста, использующего ваш класс, о том, что он допустил ошибку в передаче данных. Это концепция 'Fail Fast' (падай быстро) — лучше упасть с ошибкой в момент создания объекта, чем молча проглотить неверные данные и обрушиться через два часа работы программы в совершенно другом модуле.
Кроме валидации, метод __init__ часто используется для предварительных вычислений. Допустим, мы создаем объект Rectangle (Прямоугольник) и передаем ему ширину и высоту. Прямо в методе __init__ мы можем сразу вычислить его площадь и сохранить ее в атрибут self.area = width * height (если мы предполагаем, что прямоугольник после создания не будет менять размеры). Также здесь часто инициализируются внутренние коллекции, устанавливаются дефолтные статусы (например, self.status = 'PENDING') или генерируются уникальные идентификаторы (UUID) для нового объекта. Все это делает класс автономной и умной единицей программы.
import uuid
class BankAccount:
def __init__(self, owner_name, initial_balance=0.0):
# 1. Валидация типов и значений
if not isinstance(owner_name, str) or not owner_name.strip():
raise ValueError("Имя владельца должно быть непустой строкой.")
if initial_balance < 0:
raise ValueError("Начальный баланс не может быть отрицательным.")
# 2. Инициализация переданных атрибутов
self.owner = owner_name.strip().title()
self.balance = float(initial_balance)
# 3. Генерация внутренних/автоматических атрибутов
self.account_id = str(uuid.uuid4())[:8] # Генерируем уникальный ID
self.is_active = True
self.transactions_history = []
# Фиксируем начальный депозит в истории
if self.balance > 0:
self.transactions_history.append(f"Начальный депозит: +{self.balance}")
# Попытка создать счет с неверными данными вызовет ошибку:
# acc_error = BankAccount("", -50) # Выбросит ValueError
# Успешное создание:
acc = BankAccount("иван иванов", 150.50)
print(f"Счет {acc.account_id} для {acc.owner} создан. Баланс: {acc.balance}")
Какая концепция программирования применяется, когда мы намеренно прерываем выполнение (выбрасываем Exception) внутри __init__ при передаче неверных данных?
Какое ключевое слово в Python используется для вызова/генерации исключения внутри метода __init__ (например, если валидация провалилась)? Напишите только одно слово в нижнем регистре.
Продвинутая инициализация: *args и **kwargs в __init__
Довольно часто возникают ситуации, когда мы заранее не знаем, какое количество параметров будет передано при создании объекта, или мы хотим сделать класс максимально гибким. Как мы рассматривали в предыдущих модулях, в Python для приема произвольного числа позиционных и именованных аргументов используются конструкции *args и **kwargs. Эти инструменты можно и нужно применять при проектировании метода __init__, если это оправдано бизнес-логикой приложения.
Рассмотрим реальный сценарий (use case) из веб-разработки или работы с базами данных (Data Science). Представьте, что вы получаете данные из внешнего API в виде огромного JSON-словаря. Ключи в этом словаре могут со временем меняться, добавляться или исчезать. Вы хотите конвертировать этот словарь в объект Python, но не хотите жестко прописывать в __init__ 50 параметров. В этом случае вы можете использовать **kwargs. Внутри метода __init__ вы можете динамически перебрать все переданные пары 'ключ-значение' и превратить их в атрибуты объекта с помощью встроенной функции setattr(self, key, value). Это потрясающе мощный прием, который позволяет создавать 'резиновые' объекты, адаптирующиеся под входящие данные. Но будьте осторожны: с большой силой приходит большая ответственность. Отсутствие явных параметров снижает читаемость кода для других программистов и затрудняет автодополнение (autocomplete) в современных IDE, таких как PyCharm или VS Code.
Кроме того, *args и **kwargs активно используются в концепции наследования. Когда вы создаете дочерний класс, который расширяет родительский, его метод __init__ часто перехватывает свои специфичные параметры, а все остальные запаковывает в *args и **kwargs и пересылает 'наверх' родительскому классу с помощью вызова super().__init__(*args, **kwargs). Хотя глубокое изучение наследования ждет нас в следующих модулях, понимание того, что __init__ может работать как гибкий роутер параметров, крайне важно для перехода на уровень разработки уверенного Intermediate.
class DynamicRecord:
def __init__(self, **kwargs):
# **kwargs собирает все переданные именованные аргументы в словарь
# Мы проходимся по нему и создаем атрибуты динамически
for key, value in kwargs.items():
# setattr работает так же, как self.key = value,
# но позволяет использовать строку в качестве имени переменной
setattr(self, key, value)
def show_info(self):
# __dict__ содержит все атрибуты экземпляра
for attr, val in self.__dict__.items():
print(f"{attr} -> {val}")
# Имитируем получение данных из API (JSON)
api_response = {
"user_id": 105,
"location": "London",
"is_verified": True,
"last_login": "2023-10-15"
}
# Распаковываем словарь при создании объекта с помощью **
user_profile = DynamicRecord(**api_response)
# Теперь мы можем обращаться к данным как к атрибутам объекта
print(user_profile.location) # London
print("--- Весь профиль ---")
user_profile.show_info()
| Способ передачи параметров | Пример в __init__ | Когда использовать? |
|---|---|---|
| Позиционные (обязательные) | def __init__(self, name, age): | Для фундаментальных данных, без которых объект не может существовать. |
| С дефолтными значениями | def __init__(self, role='user'): | Для опциональных настроек и флагов. |
| Произвольные именованные | def __init__(self, **kwargs): | При парсинге внешних данных (JSON, CSV), где структура ключей нестабильна. |
Какая встроенная функция Python используется в теле __init__ для динамического создания атрибута объекта, если имя атрибута хранится в строковой переменной?
Project-Based Learning: Разработка класса для RPG игры
Давайте закрепим теорию на практике, используя методику Project-Based Learning. Мы создадим часть системы инвентаря и персонажей для фэнтезийной ролевой игры (RPG). Это отличный пример, где правильная работа метода __init__ критически важна для игровой механики. У каждого персонажа (Hero) при рождении (инстанцировании) должно быть базовое количество здоровья, пустой инвентарь, стартовое оружие и профессия. Некоторые характеристики будут вычисляться динамически на основе переданных параметров.
При проектировании такого класса нам нужно учесть множество деталей. Во-первых, нам понадобятся обязательные параметры: имя и класс (воин, маг, лучник). Во-вторых, нам потребуется опциональный параметр уровня (по умолчанию 1). В-третьих, внутри __init__ мы должны разместить логику, которая будет рассчитывать базовое здоровье в зависимости от выбранного класса персонажа. Например, воины получают больше здоровья, чем маги. В-четвертых, мы должны правильно инициализировать пустые коллекции (список заклинаний, список предметов в рюкзаке), помня о правиле избегания 'Mutable Default Arguments' (изменяемых по умолчанию). Этот комплексный пример продемонстрирует, как __init__ служит интеллектуальным диспетчером при создании сложного объекта.
Внимательно изучите следующий блок кода. Обратите внимание, как метод __init__ не просто присваивает значения, а управляет логикой ветвления (if/elif), устанавливает дефолтные пустые коллекции и готовит объект к дальнейшему использованию в боевой системе. Это пример того, что мы называем 'чистым' и 'умным' объектно-ориентированным дизайном. После изучения кода вас ждут практические вопросы для проверки (Active Recall).
class Hero:
# Вспомогательный словарь на уровне класса для базовых статов
BASE_HP = {'warrior': 150, 'mage': 80, 'archer': 110}
def __init__(self, name, hero_class, level=1, inventory=None):
self.name = name
self.hero_class = hero_class.lower()
self.level = level
# Валидация класса персонажа
if self.hero_class not in self.BASE_HP:
raise ValueError(f"Неизвестный класс героя: {hero_class}")
# Динамический расчет здоровья: Базовое ХП + (уровень * 10)
self.max_hp = self.BASE_HP[self.hero_class] + (self.level * 10)
self.current_hp = self.max_hp
# Защита от Mutable Default Arguments для инвентаря
if inventory is None:
self.inventory = ['Хлеб', 'Вода'] # Стартовый набор
else:
self.inventory = inventory
def get_status(self):
return f"[{self.hero_class.title()}] {self.name} | Ур: {self.level} | ХП: {self.current_hp}/{self.max_hp}"
# Создаем персонажей
arthas = Hero("Артас", "warrior")
jaina = Hero("Джайна", "mage", level=5, inventory=['Посох', 'Зелье маны'])
print(arthas.get_status())
print(jaina.get_status())
print(f"Инвентарь Артаса: {arthas.inventory}")
print(f"Инвентарь Джайны: {jaina.inventory}")
Задание
Модификация кода проекта: Самостоятельная практика
- Добавьте в метод __init__ атрибут self.mana, который вычисляется так: для 'mage' = 100 * уровень, для остальных = 0.
- Добавьте валидацию уровня (level), чтобы он не мог быть меньше 1 или больше 100 (вызовите ValueError).
- Измените метод get_status, чтобы он дополнительно выводил текущее количество маны.
Флеш-карточки
Можно ли внутри __init__ вызывать другие методы этого же класса (через self.method_name())?
Нажмите, чтобы увидеть ответ
Да, абсолютно. Часто большие блоки инициализации разбивают на вспомогательные методы (например, self._setup_database()), которые вызываются внутри __init__.
Нажмите, чтобы вернуться
Что должен возвращать метод __init__?
Нажмите, чтобы увидеть ответ
Метод __init__ НИКОГДА не должен содержать оператор return (кроме пустого return). Он всегда неявно возвращает None. Если попытаться вернуть значение, Python выдаст TypeError.
Нажмите, чтобы вернуться
В чем разница между атрибутом класса и атрибутом экземпляра (объекта)?
Нажмите, чтобы увидеть ответ
Атрибуты экземпляра создаются внутри __init__ через self. и уникальны для каждого объекта. Атрибуты класса создаются вне методов и делятся между всеми объектами.
Нажмите, чтобы вернуться
Ошибки проектирования: Возврат значений из __init__
Одной из специфических особенностей метода __init__, о которой часто забывают, является строгий запрет на возвращение каких-либо значений. В отличие от обычных функций или других методов класса, которые вы пишете через def и где оператор return является стандартом для передачи результатов вычислений, __init__ разработан исключительно для модификации состояния. Его единственная задача — взять свежесозданный объект (на который указывает self), наполнить его атрибутами и завершить работу. Если вы попытаетесь написать что-то вроде return True или return self.data в конце метода инициализации, интерпретатор Python немедленно сгенерирует исключение TypeError: __init__() should return None, not 'bool' (или другой тип).
Почему было принято такое архитектурное решение? Давайте разберем механику. Когда вы пишете my_obj = MyClass(), вы вызываете сам класс. Под капотом Python сначала вызывает метод __new__, который выделяет память и возвращает пустой объект. Затем Python автоматически берет этот объект и передает его в __init__ в качестве self. После того как __init__ отрабатывает, возвращаемым значением выражения MyClass() становится ТОТ САМЫЙ объект, который был создан в __new__. Если бы __init__ мог что-то возвращать, возникла бы двусмысленность: что должен вернуть вызов MyClass() — новый объект или то, что вернул __init__? Чтобы избежать путаницы, Гвидо ван Россум (создатель Python) и команда разработчиков ядра приняли решение запретить возврат значений из инициализатора. Допустим только пустой return для досрочного выхода из метода, что эквивалентно return None.
Это подводит нас к еще одному важному правилу 'чистого кода': метод __init__ не должен выполнять тяжелые бизнес-задачи. Он не должен скачивать мегабайты данных из интернета, не должен устанавливать длительные сетевые соединения или парсить гигантские файлы. Почему? Потому что это блокирует создание объекта. Создание объекта должно быть быстрым и предсказуемым. Если объекту для работы нужно подключение к базе данных, лучше создать отдельный метод, например obj.connect(), или использовать фабричные методы класса. __init__ должен заниматься только присвоением атрибутов и легкой валидацией данных. Нарушение этого правила приводит к созданию так называемых 'Fat Init' (толстых инициализаторов), что сильно затрудняет юнит-тестирование кода.
Какую ошибку (Exception) выдаст Python, если вы попытаетесь явно вернуть строку (return 'Success') из метода __init__?
Резюме модуля и лучшие практики
Давайте подведем итоги нашего глубокого погружения в тему инициализации объектов. Мы разобрали, что __init__ — это магический метод, который отвечает за первичное наполнение объекта данными, но не за его физическое создание в памяти. Мы уяснили важнейшую роль параметра self, который обеспечивает привязку создаваемых атрибутов к конкретному экземпляру класса. Мы изучили, как безопасно передавать параметры, используя *args и **kwargs, и как защититься от коварной ошибки изменяемых аргументов по умолчанию (используя None). Мы также обсудили концепцию 'Fail Fast' и валидацию данных на этапе создания объекта.
На уровне Intermediate вы должны стремиться писать методы __init__, которые легко читать и поддерживать. Чек-лист идеального __init__:
1. Он принимает разумное количество аргументов (не более 5-7). Если нужно больше, возможно, стоит использовать объекты конфигурации (Dataclasses или NamedTuples).
2. Он проверяет входящие данные и выбрасывает ValueError или TypeError, если данные не имеют смысла.
3. Он не делает тяжелой работы (например, сетевых запросов), оставляя это специализированным методам.
4. В нем нет изменяемых дефолтных аргументов ({} или [] в заголовке).
5. Все атрибуты экземпляра, которые будут использоваться в классе, хотя бы объявляются в __init__ (даже со значением None). Не стоит создавать новые атрибуты self.new_attr в других методах класса (например, в do_work()), так как это нарушает предсказуемость структуры объекта.
Объектно-ориентированное программирование — это искусство моделирования реального мира. Класс — это идея. Объект — это воплощение этой идеи. А метод __init__ — это момент рождения. Правильно заложенный фундамент при инициализации гарантирует, что ваш объект проживет долгую и безошибочную жизнь в рамках выполнения программы. В следующих уроках мы узнаем, как эти объекты могут взаимодействовать друг с другом, наследовать свойства и изменять свое поведение с помощью других магических методов.