Уроки курса
1 Введение в Python и философия дзен
30 мин
2 Переменные и динамическая типизация
30 мин
3 Базовые типы данных: числа, строки и булевы значения
30 мин
4 Изменяемые и неизменяемые объекты (Mutable vs Immutable)
30 мин
5 Форматирование строк и f-строки
30 мин
6 Углубленная работа со списками
30 мин
7 Кортежи и их особенности
30 мин
8 Словари под капотом
30 мин
9 Множества и математические операции
30 мин
10 Генераторы списков (List Comprehensions)
30 мин
11 Генераторы словарей и множеств
30 мин
12 Встроенные функции для коллекций
30 мин
13 Условные операторы и логические выражения
30 мин
14 Циклы while и управление потоком
30 мин
15 Итерация с циклом for
30 мин
16 Конструкции for...else и while...else
30 мин
17 Функции enumerate и zip
30 мин
18 Создание собственных функций (def)
30 мин
19 Позиционные и именованные аргументы
30 мин
20 Проблема изменяемых аргументов по умолчанию
30 мин
21 Произвольное число аргументов (*args и **kwargs)
30 мин
22 Область видимости переменных (LEGB)
30 мин
23 Анонимные функции (lambda)
30 мин
24 Функции высшего порядка
30 мин
25 Замыкания (Closures)
30 мин
26 Введение в объектно-ориентированное программирование
30 мин
27 Атрибуты классов и экземпляров
30 мин
28 Магический метод __init__
30 мин
29 Методы экземпляра
30 мин
30 Инкапсуляция и сокрытие данных
30 мин
31 Декоратор @property
30 мин
32 Наследование классов
30 мин
33 Переопределение методов и функция super()
30 мин
34 Полиморфизм в Python
30 мин
35 Магические методы строк (__str__ и __repr__)
30 мин
36 Обработка исключений (try-except)
30 мин
37 Блоки else и finally
30 мин
38 Генерация собственных исключений (raise)
30 мин
39 Открытие и чтение файлов
30 мин
40 Запись данных в файлы
30 мин
41 Контекстные менеджеры (with)
30 мин
42 Работа с форматом JSON
30 мин
43 Модули и импорты
30 мин
44 Полезные модули стандартной библиотеки
30 мин
45 Модуль datetime
30 мин
46 Модуль collections
30 мин
47 Виртуальные окружения (venv)
30 мин
48 Установка сторонних пакетов через pip
30 мин
49 Организация структуры Python-проекта
30 мин
50 Финальный проект: создание приложения
30 мин

Магический метод __init__

Инициализация новых объектов, передача им начальных параметров и понимание роли аргумента self.

Прогресс урока: 0%

Введение в жизненный цикл объектов: От чертежа к реальности

Добро пожаловать в двадцать восьмой урок нашего курса. До этого момента мы рассматривали классы как некие абстрактные структуры, шаблоны или чертежи. Мы говорили о том, что класс определяет общее поведение, но настоящая магия объектно-ориентированного программирования начинается в тот момент, когда абстрактный чертеж превращается в конкретный, осязаемый объект в памяти компьютера. Этот процесс называется инстанцированием или созданием экземпляра класса.

Представьте себе завод по производству автомобилей. Чертеж автомобиля — это наш класс. На чертеже указано, что у машины должны быть колеса, двигатель, цвет кузова и 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__, вы создадите обычную локальную переменную, которая бесследно исчезнет из памяти сразу после завершения работы метода инициализации, и ваш объект останется пустым. Это одна из самых частых и обидных ошибок начинающих разработчиков.

python
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 будет принимать в качестве параметров объекты этих вспомогательных классов. Это делает код более модульным, тестируемым и поддерживаемым. Помните принцип: простое лучше, чем сложное.

python
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, ...).
  • Определите атрибуты, которые создаются автоматически (например, пустой список транзакций).
10 баллов

Опасная ловушка: Изменяемые аргументы по умолчанию (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 = []. В этом случае при каждом создании объекта будет выполняться код тела метода, и для каждого объекта будет создаваться абсолютно новый, независимый пустой список. Это гарантирует изоляцию состояния объектов друг от друга, что является фундаментальным требованием объектно-ориентированного программирования.

python
# ❌ НЕПРАВИЛЬНЫЙ ПОДХОД (Ловушка изменяемого аргумента)
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__

Метод __init__ — это не просто глупый конвейер по перекладыванию переданных аргументов в атрибуты self. Это первая и главная линия обороны вашего класса. Это своего рода 'таможенный контроль', через который проходят данные, прежде чем стать частью состояния вашего объекта. В парадигме объектно-ориентированного программирования существует важное правило: объект никогда не должен находиться в неконсистентном (недействительном или противоречивом) состоянии. Если для создания объекта 'Автомобиль' требуется указать количество дверей, и кто-то передает значение '-1' или '100', класс должен немедленно заблокировать такую попытку, иначе дальнейшая логика программы будет разрушена.

Поэтому внутри __init__ крайне важно и нужно размещать логику валидации (проверки) данных. Вы можете проверять типы данных (с помощью isinstance), диапазоны значений, длину строк или соответствие формату с помощью регулярных выражений. Если переданные данные некорректны, лучшей практикой в Python является немедленное выбрасывание исключения (raise Exception), например, ValueError или TypeError. Это прерывает процесс создания объекта (объект не будет создан) и информирует программиста, использующего ваш класс, о том, что он допустил ошибку в передаче данных. Это концепция 'Fail Fast' (падай быстро) — лучше упасть с ошибкой в момент создания объекта, чем молча проглотить неверные данные и обрушиться через два часа работы программы в совершенно другом модуле.

Кроме валидации, метод __init__ часто используется для предварительных вычислений. Допустим, мы создаем объект Rectangle (Прямоугольник) и передаем ему ширину и высоту. Прямо в методе __init__ мы можем сразу вычислить его площадь и сохранить ее в атрибут self.area = width * height (если мы предполагаем, что прямоугольник после создания не будет менять размеры). Также здесь часто инициализируются внутренние коллекции, устанавливаются дефолтные статусы (например, self.status = 'PENDING') или генерируются уникальные идентификаторы (UUID) для нового объекта. Все это делает класс автономной и умной единицей программы.

python
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.

python
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).

python
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, чтобы он дополнительно выводил текущее количество маны.
10 баллов

Ошибки проектирования: Возврат значений из __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__ — это момент рождения. Правильно заложенный фундамент при инициализации гарантирует, что ваш объект проживет долгую и безошибочную жизнь в рамках выполнения программы. В следующих уроках мы узнаем, как эти объекты могут взаимодействовать друг с другом, наследовать свойства и изменять свое поведение с помощью других магических методов.

Если мы хотим создать атрибут внутри __init__, который будет содержать список, и этот список должен быть пустым по умолчанию при создании объекта. Какое значение мы должны указать в параметрах метода __init__ для этого аргумента? (Напишите одно слово)

Что означает принцип 'Fail Fast' применительно к методу __init__?