Skip to content

Latest commit

 

History

History
470 lines (315 loc) · 34 KB

File metadata and controls

470 lines (315 loc) · 34 KB

Урок 4. Инициализатор __init__ и финализатор __del__

В прошлом уроке был разобран механизм вызова методов и роль параметра self. Но до сих пор объект после создания оставался пустым — чтобы он получил хоть какие-то данные, приходилось вызывать отдельный метод вроде set_coords, и до этого вызова любое обращение к координатам заканчивалось исключением AttributeError. Это неудобно и, что важнее, ненадёжно: ничто не мешает забыть вызвать нужный метод и получить объект в некорректном, «недособранном» состоянии.

Python решает эту проблему с помощью специального метода __init__, который вызывается автоматически сразу после создания объекта и гарантирует, что к моменту, когда объект попадёт в руки программиста, он уже полностью готов к работе. В этом уроке разбираются два метода, отвечающих за противоположные концы жизненного цикла объекта: __init__, вызываемый при рождении объекта, и __del__, вызываемый перед его уничтожением. Оба относятся к особой категории методов Python, которые называют магическими.

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

Магическими называют методы, чьё имя обрамлено двумя парами подчёркиваний с обеих сторон — __init__, __del__, __str__, __add__ и десятки других. Их отличительная черта в том, что программист почти никогда не вызывает их напрямую по имени. Вместо этого Python вызывает их сам, реагируя на определённые события: создание объекта, попытка сложить два объекта оператором +, преобразование объекта в строку и так далее. Из-за этой связи с встроенным поведением языка их иногда называют dunder-методами (от double underscore), а в русскоязычной литературе закрепилось название «магические» — не потому что в них есть что-то мистическое, а потому что их вызов происходит неявно, «по волшебству», без явного обращения через точку.

В этом уроке речь пойдёт о двух таких методах, отвечающих за две границы существования объекта:

Метод Когда вызывается
__init__ сразу после создания объекта — инициализация
__del__ непосредственно перед уничтожением объекта — финализация

Проблема, которую решает __init__

Вернёмся к классу Point из предыдущего урока:

class Point:
    color = "red"
    circle = 2

    def set_coords(self, x, y):
        self.x = x
        self.y = y

    def get_coords(self):
        return (self.x, self.y)

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

pt = Point()
pt.get_coords()
AttributeError: 'Point' object has no attribute 'x'

Ошибка ожидаема: атрибуты x и y создаются только внутри set_coords, а этот метод ещё не вызывался. Проблема здесь не только в неудобстве — она в том, что между моментом создания объекта и моментом вызова set_coords объект существует в состоянии, для которого класс, по сути, не был спроектирован: get_coords его не переживёт. Гораздо надёжнее было бы гарантировать, что объект получает все необходимые данные ещё в момент создания, и не может существовать без них вовсе.

Метод __init__

Именно эту гарантию даёт метод __init__. Python вызывает его автоматически сразу после того, как объект создан — то есть строка Point() не просто выделяет память под новый объект, но и немедленно передаёт этот объект в __init__ для донастройки.

class Point:
    color = "red"
    circle = 2

    def __init__(self):
        print("Вызов метода __init__")
        self.x = 0
        self.y = 0
pt = Point()
Вызов метода __init__
print(pt.__dict__)   # {'x': 0, 'y': 0}

Обратите внимание: между строкой pt = Point() и следующей строкой кода программист не сделал ничего — а атрибуты x и y уже существуют. Их создал __init__, вызванный автоматически как неотъемлемая часть процесса создания объекта. С этого момента невозможно получить объект Point, у которого координаты отсутствовали бы: __init__ — обязательный этап, который Python выполняет для каждого создаваемого объекта, если только в классе объявлен этот метод.

Что происходит при вызове Point()

Полезно понимать создание объекта не как одно неделимое действие, а как последовательность из нескольких этапов. Когда интерпретатор встречает вызов Point(), происходит следующее: сначала специальный метод __new__ выделяет память и создаёт «сырой», ещё не настроенный объект нужного типа; затем этот только что созданный объект автоматически передаётся в __init__, который заполняет его нужными данными; и только после этого выражение Point() возвращает полностью готовый объект вызывающему коду.

__new__ отвечает за само создание объекта, а __init__ — за его настройку уже после того, как объект существует. В подавляющем большинстве классов, которые пишут на практике, __new__ вообще не переопределяется — Python использует стандартную реализацию, унаследованную от базового класса object, а вся содержательная работа выполняется в __init__. Подробно __new__ будет разобран в следующем уроке — там же станет понятно, зачем вообще может понадобиться вмешиваться в этап создания объекта отдельно от его инициализации.

Параметры __init__

__init__ — это метод, а значит, обычная функция с параметром self первым в списке. Ничто не мешает добавить и другие параметры, через которые снаружи будут переданы данные для настройки объекта:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
pt = Point(1, 2)
print(pt.__dict__)   # {'x': 1, 'y': 2}

Здесь стоит зафиксировать, что именно происходит при вызове Point(1, 2). Значения 1 и 2, указанные в скобках, — это не аргументы «класса» в каком-то абстрактном смысле, а обычные аргументы, которые Python передаёт в __init__ вслед за автоматически подставленным self. То есть вызов Point(1, 2) фактически равносилен тому, что где-то внутри был бы выполнен вызов __init__(новый_объект, 1, 2) — та же схема передачи self, что была разобрана применительно к обычным методам в прошлом уроке, полностью применима и здесь: __init__ — это метод, как и любой другой, просто вызываемый Python-ом автоматически в строго определённый момент.

Имена параметров при этом могут быть произвольными — Python не требует, чтобы параметр x обязательно записывался именно в атрибут x:

class Point:
    def __init__(self, a, b):
        self.x = a
        self.y = b

Такой код будет работать совершенно корректно. Но общепринятая практика — называть параметр так же, как и атрибут, в который он записывается. Она не диктуется языком, а лишь избавляет читающего код человека от необходимости мысленно сопоставлять одно имя с другим, и почти весь код, который вам предстоит читать в реальных проектах, следует именно ей.

Значения по умолчанию в __init__

Поскольку __init__ — обычная функция, к её параметрам применимы все инструменты, известные из работы с функциями, включая значения по умолчанию:

class Point:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
pt1 = Point()          # (0, 0)
pt2 = Point(10)         # (10, 0)
pt3 = Point(10, 20)     # (10, 20)

Значения по умолчанию делают часть аргументов необязательными, позволяя создавать объект разными способами в зависимости от того, сколько данных известно программисту в конкретной ситуации — от полностью настроенного объекта до объекта с разумными значениями «по умолчанию».

Более реалистичный пример

Рассмотрим класс, приближённый к тем, что встречаются в реальных задачах:

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def show_info(self):
        print("Имя:", self.name)
        print("Возраст:", self.age)
user = User("Иван", 25)
user.show_info()
Имя: Иван
Возраст: 25

Класс User невозможно создать без имени и возраста — оба параметра обязательны, значений по умолчанию для них нет. Это осознанное архитектурное решение: пользователь без имени концептуально не имеет смысла, и __init__ здесь выступает не просто удобством, а гарантией целостности объекта — компилятор (точнее, интерпретатор) физически не позволит создать объект User, минуя передачу обязательных данных.

Финализатор __del__

Если __init__ отвечает за рождение объекта, то __del__ — за противоположный момент, за его уничтожение. Python вызывает этот метод непосредственно перед тем, как объект будет удалён из памяти.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __del__(self):
        print("Удаление объекта:", self)
pt = Point(1, 2)
del pt
Удаление объекта: <__main__.Point object at 0x...>

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

Когда объект действительно удаляется

В основе управления памятью в CPython (стандартной и наиболее распространённой реализации Python) лежит подсчёт ссылок. У каждого объекта в памяти есть счётчик — сколько мест в программе сейчас на него ссылаются. Когда переменная pt присваивается объекту Point(1, 2), счётчик ссылок этого объекта равен единице. Как только счётчик ссылок опускается до нуля — то есть ни одна переменная, ни один список, ни одна структура данных больше не хранит ссылку на объект, — Python считает объект недостижимым и удаляет его из памяти, вызывая перед этим __del__.

Оператор del pt не удаляет объект напрямую — он удаляет саму переменную pt, то есть убирает одну из ссылок на объект. Если это была последняя оставшаяся ссылка, счётчик обнуляется, и объект уничтожается. Но если на тот же объект где-то ещё сохранена ссылка, del pt уничтожит только имя pt, а сам объект продолжит существовать в памяти:

pt = Point(1, 2)
pt2 = pt          # вторая ссылка на тот же объект

del pt            # удаляется только имя pt, объект жив — на него ссылается pt2

print(pt2.x)       # 1 — объект всё ещё доступен

__del__ в этом примере не вызовется в момент del pt, потому что счётчик ссылок объекта после этой операции равен единице, а не нулю — объект остаётся достижимым через pt2.

Подводные камни

Здесь начинается самая важная часть урока — практические ограничения __del__, из-за которых этот метод почти не используется в реальном коде, несмотря на кажущуюся полезность.

Первая проблема — непредсказуемость момента вызова. Обнуление счётчика ссылок само по себе предсказуемо, но программист редко может быть полностью уверен, что на объект больше нигде не осталось ссылок — особенно в крупной программе, где объект мог быть передан в другую функцию, сохранён в кеше или замыкании. Полагаться на __del__ для выполнения важного действия «прямо сейчас» — например, для закрытия файла или сетевого соединения — значит полагаться на момент времени, который сложно проконтролировать.

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

class Node:
    def __init__(self, name):
        self.name = name
        self.partner = None

    def __del__(self):
        print(f"Удаление объекта {self.name}")


a = Node("A")
b = Node("B")

a.partner = b
b.partner = a

del a
del b

После выполнения del a и del b счётчик ссылок ни у одного из объектов не обнуляется сразу: объект a по-прежнему хранит ссылку на b через a.partner, а b хранит ссылку на a через b.partner, даже несмотря на то что внешних переменных, указывающих на них, больше нет. Подсчёт ссылок в такой ситуации бессилен — с его точки зрения оба объекта всё ещё нужны друг другу. Именно для подобных случаев в CPython существует отдельный механизм — циклический сборщик мусора (cyclic garbage collector), который периодически ищет группы взаимно ссылающихся друг на друга объектов, недостижимые извне, и удаляет их целиком. Но момент, когда сборщик мусора решит запустить такую проверку, заранее неизвестен — он может произойти сразу, а может значительно позже, в зависимости от внутренней логики интерпретатора.

Третья проблема связана с завершением программы. Если программа завершается аварийно или в процессе завершения интерпретатора часть модулей уже выгружена из памяти, __del__ может либо не вызваться вовсе, либо вызваться в момент, когда некоторые объекты, на которые он опирается (например, глобальные переменные модуля), уже недоступны, что приводит к дополнительным исключениям внутри самого __del__ — исключениям, которые Python в большинстве случаев просто молча проигнорирует, лишь выведя предупреждение.

По совокупности этих причин __del__ крайне редко применяется в реальных проектах для управления важными ресурсами. Задачи вроде закрытия файлов, освобождения сетевых соединений или закрытия транзакций базы данных решаются через контекстные менеджеры — конструкцию with, которая гарантирует выполнение кода очистки в чётко определённый момент, независимо от сборщика мусора. Контекстные менеджеры будут подробно разобраны позже, в уроке, посвящённом методам __enter__ и __exit__.

Тем не менее __del__ не бесполезен полностью — он находит применение там, где точность момента вызова некритична: в отладочном логировании (чтобы убедиться, что объект действительно был удалён и не «утёк» в памяти), а также как последняя подстраховка при освобождении ресурса на случай, если разработчик забыл вызвать явный метод закрытия. Но даже в такой роли __del__ рассматривается как страховка на крайний случай, а не как основной механизм.

Итоги урока

__init__ и __del__ относятся к магическим методам — методам, которые Python вызывает автоматически в ответ на определённые события, а не по прямому обращению программиста. __init__ вызывается сразу после создания объекта функцией __new__ и отвечает за его настройку: именно здесь принято создавать все атрибуты, без которых объект не может считаться полноценным. Благодаря __init__ невозможно получить объект в «недоинициализированном» состоянии — в отличие от подхода прошлого урока, где координаты точки появлялись только после отдельного вызова set_coords.

__del__ вызывается перед уничтожением объекта — то есть в момент, когда счётчик ссылок на него обнуляется. Управление памятью в CPython строится на подсчёте ссылок, дополненном циклическим сборщиком мусора для случаев взаимных ссылок, которые подсчёт ссылок сам по себе разрешить не способен. Из-за непредсказуемости момента вызова, сложностей с циклическими ссылками и ненадёжности при аварийном завершении программы __del__ почти никогда не используется для управления критичными ресурсами — эту роль в реальном коде выполняют контекстные менеджеры.

В следующем уроке мы разберём магический метод __new__ — тот самый первый этап создания объекта, который был упомянут в этом уроке лишь вскользь, — и увидим, как управление им позволяет реализовать паттерн Singleton, гарантирующий существование лишь одного экземпляра класса.


Вопросы для проверки

  1. Что объединяет __init__ и __del__ с точки зрения того, как они вызываются?
  2. Какую проблему решает __init__ по сравнению с подходом, где данные объекта устанавливаются отдельным методом вроде set_coords?
  3. Из каких этапов состоит создание объекта при вызове Point(...)?
  4. Почему self обязательно должен быть первым параметром __init__?
  5. Обязаны ли все параметры __init__ иметь значения по умолчанию?
  6. Что произойдёт при попытке создать объект, не передав обязательный аргумент __init__?
  7. При каком условии Python вызывает __del__ у объекта?
  8. Почему __del__ считается ненадёжным способом освобождения важных ресурсов?
  9. Почему подсчёт ссылок сам по себе не способен удалить два объекта, ссылающихся друг на друга?
  10. Чем задачи, которые решает __del__, отличаются от задач, для которых на практике используют контекстные менеджеры?

Задачи

Задача 1.

Объявите класс Money так, чтобы объекты создавались командой Money(сумма), где переданное число сохраняется в атрибуте объекта money.

Ожидаемый результат для Money(100) и Money(1000):

100
1000

Задача 2.

Объявите класс Point, объекты которого создаются как Point(x, y) или Point(x, y, color). Если цвет не передан, он должен принимать значение "black" по умолчанию.

Создайте список points из 1000 объектов с координатами (1, 1), (3, 3), (5, 5), ... — каждая следующая точка увеличивает обе координаты на 2 по сравнению с предыдущей. Второму объекту списка присвойте цвет "yellow" уже после создания.


Задача 3.

Объявите три класса — Line, Rect, Ellipse, объекты которых создаются одинаково: Line(a, b, c, d). При создании в объекте должны появляться атрибуты sp = (a, b) и ep = (c, d).

Создайте список elements из 213 объектов случайно выбранных классов (Line, Rect или Ellipse) со случайными координатами в диапазоне от 0 до 100. После этого обнулите координаты (sp и ep) только у объектов класса Line.


Задача 4.

Создайте класс User, при создании объекта которого (User(name)) выводится сообщение Пользователь <name> создан, а перед удалением — Пользователь <name> удалён.

Создайте список users из трёх объектов User, удалите один объект из списка, затем принудительно удалите переменную, содержащую список.


Задача 5.

Объявите класс TriangleChecker, объекты которого создаются как TriangleChecker(a, b, c), где a, b, c — длины сторон. Реализуйте метод is_triangle(), который проверяет условия строго по порядку и возвращает:

  • 1 — если хотя бы одна из сторон не является числом или меньше либо равна нулю;
  • 2 — если такие стороны не могут образовать треугольник (нарушено неравенство треугольника);
  • 3 — если стороны образуют корректный треугольник.

Задача 6

Создайте три класса:

  • CPU
  • Memory
  • MotherBoard

Объекты создаются следующим образом:

cpu = CPU(name, frequency)
mem = Memory(name, volume)
mb = MotherBoard(name, cpu, mem1, mem2, ...)

Каждый объект должен хранить:

CPU

name
fr

Memory

name
volume

MotherBoard

name
cpu
mem_slots
total_mem_slots = 4

Список mem_slots должен содержать не более 4 объектов Memory. Если при создании передать большее количество оперативной памяти, то mem_slots должен будет содержать только первые 4 из них.

Класс MotherBoard должен иметь метод get_config() который возвращает список строк:

Материнская плата: <name>
Центральный процессор: <cpu_name>, <frequency>
Слотов памяти: 4
Память: name1 - volume1; name2 - volume2

Создайте материнскую плату с 1 CPU и 2 планками памяти

Выведите результат get_info() на экран.

Создайте материнскую плату с 1 CPU и 5 планками памяти.

Выведите результат get_info() на экран.


Задача 7.

В программировании часто используются специальные структуры данных для хранения и обработки информации. Одна из самых базовых структур — связный список.

Односвязный список (Singly Linked List) — это последовательность объектов, где каждый объект хранит:

  1. данные
  2. ссылку на следующий объект

В отличие от обычного списка Python (list), элементы связного списка не хранятся в одной структуре. Каждый элемент существует как отдельный объект, который знает только о следующем элементе.

Схематично это выглядит так:

head_obj
   |
   v
+---------+      +---------+      +---------+      +---------+
| data    | ---> | data    | ---> | data    | ---> | data    |
| next_obj|      | next_obj|      | next_obj|      | next_obj|
+---------+      +---------+      +---------+      +---------+
      |                |                |               |
      v                v                v               v
    объект2         объект3          объект4          None
  • Первый объект называется головой списка (head).
  • Каждый объект хранит ссылку на следующий объект.
  • Последний элемент содержит next_obj = None.

Условие Задачи:

Объявите в программе класс ListObject, объекты которого создаются командой:

obj = ListObject(data)

где data — строка, которая должна сохраняться внутри объекта.


Каждый объект класса ListObject должен иметь два локальных атрибута:

1. data строка, переданная при создании объекта.

2. next_obj ссылка на следующий объект списка.

Если следующий объект отсутствует, значение должно быть:

None

В классе ListObject необходимо реализовать метод:

link(self, obj)

Назначение метода:

соединить текущий объект self со следующим объектом obj.

После вызова метода:

self.next_obj → obj

Далее в программе необходимо сформировать односвязный список используя строки из следующего списка:

lst_in = [
    'Введение в ООП',
    '1 Концепция ООП простыми словами',
    '2 Классы и объекты. Атрибуты классов и объектов',
    '3 Методы класса и параметр `self`',
    '4 Инициализатор init и финализатор del'
]

Ваша задача:

  1. Для каждой строки необходимо создать объект класса ListObject.
  2. В атрибуте data каждого объекта должна храниться соответствующая строка.
  3. Все объекты должны быть последовательно соединены друг с другом через метод link().
  4. Переменная head_obj должна ссылаться на первый объект списка.

Предыдущий урок | Следующий урок