Всё, что нужно знать о конструкторах в Python
hard

Всё, что нужно знать о конструкторах в Python

Ну или почти всё

Как и в любом другом языке программирования, конструкторы в Python предназначены для инициализации новых объектов. Неправильное использование конструкторов может привести к различным проблемам в программе. Рассказываем подробнее.

Что такое конструктор

Конструктор — это метод, который используется для создания и инициализации объектов класса и определён в самом классе. Стандартный конструктор в Python обозначается как __init__ и имеет синтаксис:

def __init__(self):

Каждый раз, когда определяется метод объекта для класса, в качестве первого параметра используется self — это ссылка на текущий объект. С помощью self можно получить доступ к переменной и методу объекта. Не обязательно называть первый параметр self, можно дать ему любое имя, но оно должно быть указано первым параметром.

Если конструктор не определён, то есть не включён в какой-то класс или класс не объявлен, при создании объекта конструктор и инструкции в нём вызываются и выполняются автоматически. Такой конструктор пустой и не возвращает какое-либо значение, в том числе None, и не выполняет никаких задач, только инициализирует объект. Например, при выполнении кода 

object = Example()

Python понимает, что obj — объект класса Example, и вызывает для создания объекта конструктор этого класса.

Какие бывают конструкторы

Как и в случае с неопределённым конструктором, если конструктор не объявлен в своём классе, Python вставит в код конструктор по умолчанию. Он сделает это во время компиляции, то есть в исходном файле .py такой конструктор может отсутствовать.

Конструктор без аргументов называется непараметризованным. Он имеет стандартный синтаксис и инициализирует каждый объект с одним и тем же набором значений:

class Media:
   def __init__(self):
       self.type = 'Журнал'
       self.name = '"Код"'

   def show(self):
       print('Вид издания:', self.type, 'Название:', self.name)

media = Media()
media.show()

Конструктор с определёнными аргументами называется параметризованным. С его помощью во время создания объектов можно передавать им разные значения. Параметризованный конструктор может иметь любое количество аргументов: сколько объявим, столько и будет:

class Employee:
    def __init__(self, name, surname, position):
        self.name = name
        self.surname = surname
        self.position = position

    def show(self):
        print(self.name, self.surname, self.position)

ira = Employee('Ира', 'Михеева', 'корректор')
ira.show()

masha = Employee('Маша', 'Климентьева', 'Руководитель группы контент-менеджеров')
masha.show()

Инициализация параметров объекта

Чтобы избежать ошибок, инициализировать параметры нужно до вызова методов, которые могут зависеть от этих параметров.

Самая простая инициализация — когда параметры объекта инициализируются непосредственно значениями, переданными в конструктор:

class Editorial:
    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    def show(self):
       print(self.name, self.surname)

mike = Editorial('Михаил', 'Полянин')
mike.show()

Ещё в конструктор можно добавить проверку значений:

class Employee:
    def __init__(self, name, surname, since):
        if not isinstance(name, str):
            raise ValueError('Имя должно быть строкой')
        if not (2019 <= since <= 2024):
            raise ValueError('Год начала работы должен быть в диапазоне 2019–2024')
        self.name = name
        self.surname = surname
        self.since = since

    def show(self):
       print(self.name, self.surname, self.since)

mike = Employee('Михаил', 'Полянин', 2019)
mike.show()

Можно задать значения по умолчанию для некоторых параметров, чтобы сделать их необязательными при создании объекта, — они будут использоваться, если не передать эти параметры конструктору во время создания объекта:

class Editorial:
    def __init__(self, name, surname, position='автор'):
        self.name = name
        self.surname = surname
        self.position = position

    def show(self):
        print(self.name, self.surname, self.position)

christina = Editorial('Кристина', 'Тульцева')
christina.show()

mike = Editorial('Михаил', 'Полянин', 'главный редактор')
mike.show()

Параметры могут быть не только простыми типами данных, но и сложными объектами, такими как списки, словари или даже представители других классов.

class Media:
   def __init__(self, type, name):
       self.type = type
       self.name = name

class Editorial:
   def __init__(self, name, surname, position, media):
       self.name = name
       self.surname = surname
       self.position = position
       self.media = media

   def show(self):
       print(self.name, self.surname, self.position, self.media.type, self.media.name)

media = Media('Журнал', '"Код"')
mike = Editorial('Михаил', 'Полянин', 'главный редактор', media)
mike.show()

Иногда полезно вынести часть инициализации в отдельные методы для упрощения конструктора и улучшения читаемости кода:

class Employee:
   def __init__(self, name, surname, since):  # Замена точки на запятую
       self.name = name
       self.surname = surname
       self.set_since(since)

   def set_since(self, since):
       if not (2019 <= since <= 2024):
           raise ValueError('Год начала работы должен быть в диапазоне 2019–2024')
       self.since = since

ira = Employee('Ира', 'Михеева', 2019)

Если есть несколько вариантов инициализации, нужно использовать внутренние методы во избежание дублирования кода:

class MyClass:
    def __init__(self, value, flag):
        if flag:
            self._initialize_with_flag(value)
        else:
            self._initialize_without_flag(value)
    
    def _initialize_with_flag(self, value):
        self.value = value * 2
    
    def _initialize_without_flag(self, value):
        self.value = value

Дополнительные методы инициализации

Иногда бывает полезно создавать объекты через классовые методы, особенно если требуется сложная логика создания объектов. Например, когда нужно предоставить несколько способов инициализации объектов или когда необходимо выполнить предварительную обработку данных перед созданием объекта. Классовый метод обозначается специальным декоратором classmethod и, в отличие от обычных методов, первым аргументом принимает класс cls вместо self. Это позволяет работать с классом и его параметрами, а не с конкретным объектом:

# определяем класс и его параметры
class MyClass:
    def __init__(self, value):
        self.value = value

    # применяем декоратор
    @classmethod
    def from_alternate_constructor(cls, alt_value):
        # Какая-то сложная логика
        value = alt_value * 2
        return cls(value)

obj = MyClass.from_alternate_constructor(10)

В некоторых случаях полезно использовать метод __post_init__, который вызывается после основного конструктора. Он используется вместе с декоратором @dataclass и позволяет выполнять дополнительные действия после автоматической инициализации параметров, определённых в классе с @dataclass.

# подключаем модуль dataclasses
from dataclasses import dataclass, field

# определяем класс и его параметры
@dataclass
class MyClass:
    value: int
    extra: int = field(init=False)

# добавляем метод __post_init__
    def __post_init__(self):
        self.extra = self.value * 2

Конструкторы и наследование

Наследование в Python позволяет создавать новый класс на основе существующего. Новый класс, называемый дочерним (или подклассом), наследует параметры и методы родительского класса. 

Когда создаётся родительский класс, в нём часто определяется конструктор для инициализации параметров:

class Animal:
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

    def make_sound(self):
        pass

Если дочерний класс не переопределяет конструктор, он автоматически наследует конструктор родительского класса:

class Animal:
   def __init__(self, name, breed):
       self.name = name
       self.breed = breed

   def make_sound(self):
       pass

class Dog(Animal):
   def make_sound(self):
       return 'Гав'
dog = Dog(name='Себастиан', breed='ротвейлер')
print(dog.name)  # Себастиан
print(dog.breed)  # ротвейлер
print(dog.make_sound())  # Гав!

Иногда нужно, чтобы дочерний класс имел свой собственный конструктор. В этом случае необходимо вызывать конструктор родительского класса с помощью функции super() для инициализации его части:

class Animal:
   def __init__(self, name, breed):
       self.name = name
       self.breed = breed

   def make_sound(self):
       pass

class Dog(Animal):
   def __init__(self, name, breed, age):
       # Вызов конструктора родительского класса
       super().__init__(name, breed)
       self.age = age

   def make_sound(self):
       return 'Woof!'

dog = Dog(name="Себастиан", breed="ротвейлер", age=5)
print(dog.name)  # Себастиан
print(dog.breed)  # ротвейлер
print(dog.age)  # 5

Конструктор дочернего класса может принимать дополнительные аргументы, необходимые для инициализации новых параметров:

class Animal:
   def __init__(self, name, breed):
       self.name = name
       self.breed = breed

   def make_sound(self):
       pass

class Bird(Animal):
   def __init__(self, name, breed, can_fly):
       # Вызов конструктора родительского класса
       super().__init__(name, breed)
       self.can_fly = can_fly

   def make_sound(self):
       return "Чик-чирик!"

bird = Bird(name="Твити", breed="канарейка", can_fly=True)
print(bird.name)  # Твити
print(bird.breed)  # канарейка
print(bird.can_fly)  # True

В Python поддерживается множественное наследование — это означает, что класс может наследовать от нескольких родительских классов. В этом случае необходимо аккуратно вызывать конструкторы всех родительских классов.

class A:
    def __init__(self, a):
        self.a = a

class B:
    def __init__(self, b):
        self.b = b

class C(A, B):
    def __init__(self, a, b, c):
        # Явный вызов конструктора класса A
        A.__init__(self, a)
        # Явный вызов конструктора класса B
        B.__init__(self, b)
        self.c = c

c = C(a=1, b=2, c=3)
print(c.a)  # 1
print(c.b)  # 2
print(c.c)  # 3

Также есть альтернативный способ с использованием super():

class A:
    def __init__(self, a):
        self.a = a

class B(A):
    def __init__(self, a, b):
        super().__init__(a)
        self.b = b

class C(B):
    def __init__(self, a, b, c):
        super().__init__(a, b)
        self.c = c

c = C(a=1, b=2, c=3)
print(c.a)  # 1
print(c.b)  # 2
print(c.c)  # 3

При использовании множественного наследования важно учитывать порядок вызова конструкторов всех классов. В этом может помочь метод разрешения порядка MRO (Method Resolution Order), который можно проверить с помощью ClassName.mro().

Частые ошибки при использовании конструкторов

Перегрузка конструктора. Если определить несколько конструкторов, интерпретатор будет рассматривать только последний конструктор и выдаст ошибку, если последовательность параметров не совпадёт с последним конструктором:

class Employee:
    # конструктор с одним параметром
    def __init__(self, name):
        self.name = name

    # конструктор с двумя параметрами
    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

ira = Employee('Ира')

masha = Employee('Маша', 'Климентьева')

При выполнении такой код выдаст ошибку:

TypeError: Employee.__init__() missing 1 required positional argument: 'surname'

Отсутствие вызова super() в наследуемом классе. Когда класс наследуется от другого класса, инициализация родительского класса должна быть выполнена явно. Такой код выдаст ошибку:

class Parent:
    def __init__(self, value):
        self.value = value

class Child(Parent):
    def __init__(self, value, extra):
        # Ошибка: отсутствие super()
        # self.value = value
        super().__init__(value)
        self.extra = extra

Неправильное использование аргументов. Если забыть про обязательные аргументы или неправильно их использовать в конструкторе, это может привести к ошибкам:

class MyClass:
    def __init__(self, value1, value2):
        self.value1 = value1
        self.value2 = value2

# Ошибка: недостаточно аргументов
obj = MyClass(10)  # TypeError

Изменение параметров по умолчанию, если они являются изменяемыми объектами. Если параметр по умолчанию является изменяемым объектом (например, список или словарь), это может привести к неожиданному поведению:

class MyClass:
    def __init__(self, values=[]):
        self.values = values

obj1 = MyClass()
obj2 = MyClass()
obj1.values.append(1)
# [1], а не []
print(obj2.values)

Вызов методов из конструктора. Если метод, вызываемый из конструктора, зависит от параметров, которые ещё не инициализированы, это может привести к ошибкам:

class Example:
   def __init__(self, value):
       self.process_value()
       self.value = value

   def process_value(self):
       print(self.value)

example = Example(42)

Код выдаст ошибку:

AttributeError: 'Example' object has no attribute 'value'

Обложка:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Маша Климентьева

Соцсети:

Юлия Зубарева

Любишь Python? Зарабатывай на нём!
Изучите самый модный язык программирования и станьте крутым бэкенд-разработчиком. Старт — бесплатно.
Попробовать бесплатно
Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём!
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
hard