Атрибуты и специальные методы
В прошлом уроке все данные объекта жили в атрибутах вроде self.name — у каждого объекта свои. Сегодня разберём данные, общие для всех объектов класса, обезвредим классическую ловушку в __init__ и научим объекты печататься и сравниваться по-человечески.
Атрибуты экземпляра и атрибуты класса
Атрибуты, которые создаёт __init__ через self.имя = ..., называются атрибутами экземпляра: у каждого объекта своё значение. Но бывают данные, одинаковые для всех. Пусть все студенты учатся в одной школе — хранить её название в каждом объекте нет смысла: значение одно на всех, а поменяется — придётся обновлять в каждом объекте.
Такие данные объявляют прямо в теле класса, вне методов. Это атрибут класса:
Python 3.13class Student: school = "Школа №1" # атрибут класса — один на всех def __init__(self, name): self.name = name # атрибут экземпляра — у каждого свой student1 = Student("Алексей") student2 = Student("Екатерина") print(f"{student1.name}, {student1.school}")Алексей, Школа №1print(f"{student2.name}, {student2.school}")Екатерина, Школа №1
Заметьте: student1.school читается, хотя в самом объекте такого атрибута нет. Не найдя имя в экземпляре, Python ищет его в классе. Поэтому достаточно поменять атрибут класса — и изменение увидят сразу все объекты:
Python 3.13class Student: school = "Школа №1" def __init__(self, name): self.name = name student1 = Student("Алексей") student2 = Student("Екатерина") Student.school = "Школа №5" print(f"{student1.name}, {student1.school}")Алексей, Школа №5print(f"{student2.name}, {student2.school}")Екатерина, Школа №5
name — у каждого объекта свой, а school хранится в классе: один на всехПравило выбора простое: то, что различается между объектами, — в атрибуты экземпляра (имя, возраст), а то, что одно на всех, — в атрибуты класса (константы, общие настройки, значения по умолчанию).
Ловушка с изменяемыми значениями по умолчанию
Эта ловушка в __init__ ловит даже опытных разработчиков. Допустим, мы хотим, чтобы у студента по умолчанию был пустой список оценок. Кажется логичным написать так:
Python 3.13class Student: def __init__(self, name, grades=[]): # выглядит безобидно self.name = name self.grades = grades s1 = Student("Анна") s1.grades.append(5) print(s1.grades)[5]s2 = Student("Иван") print(s2.grades) # ожидаем [][5]
У Ивана оказалась оценка Анны, хотя мы ничего ему не добавляли. Почему?
Значения параметров по умолчанию вычисляются один раз, в момент определения функции, а не при каждом вызове. Список [] создан один раз и переиспользуется всеми вызовами Student(...) без аргумента grades. То есть s1.grades и s2.grades указывают на один и тот же список в памяти.
Правильный паттерн: по умолчанию ставить None — знакомый по прошлому уроку приём, — а реальный список создавать внутри:
Python 3.13class Student: def __init__(self, name, grades=None): self.name = name if grades is None: grades = [] self.grades = grades s1 = Student("Анна") s1.grades.append(5) print(s1.grades)[5]s2 = Student("Иван") print(s2.grades)[]
Теперь каждый объект получает свой собственный пустой список. Тот же приём работает для словарей, множеств и любых других изменяемых значений.
Специальные методы
Данные разложены по атрибутам — посмотрим, как объект ведёт себя в привычных операциях. Напечатаем его и сравним два одинаковых:
Python 3.13class Vector: def __init__(self, x, y): self.x = x self.y = y v1 = Vector(3, 4) v3 = Vector(3, 4) print(v1)<__main__.Vector object at 0x7f9b1c2d3e50>print(v1 == v3)False
Печать выдала адрес объекта в памяти вместо содержимого, а сравнение ответило False при совпадающих координатах: без нашей помощи Python сравнивает объекты по тому, один ли это объект в памяти, а не по содержимому.
Обе проблемы решаются специальными методами — методами с двойными подчёркиваниями в имени, которые Python вызывает сам в нужный момент:
- __init__ вы уже пишете в каждом классе: он срабатывает при Student(...);
- __str__ срабатывает, когда объект нужно превратить в строку — например, внутри print();
- __eq__ — при сравнении через ==;
- __add__ — при сложении через +.
Определим три новых метода в Vector:
Python 3.13class Vector: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"Vector({self.x}, {self.y})" def __add__(self, other): return Vector(self.x + other.x, self.y + other.y) def __eq__(self, other): return self.x == other.x and self.y == other.y v1 = Vector(3, 4) v2 = Vector(1, 2) v3 = Vector(3, 4) print(v1)Vector(3, 4)print(v1 + v2)Vector(4, 6)print(v1 == v3)True
Обратите внимание на __add__: он не меняет исходные векторы, а собирает из их координат новый объект и возвращает его.
Спецметодов в Python десятки: __len__ научит объект отвечать на len(...), __repr__ — показывать себя в отладке. Учить их списком не нужно — они будут встречаться по курсу по мере надобности.
Проверка понимания
Программа создала двух студентов. Что напечатает последняя строка?
Python 3.13class Student: def __init__(self, name, grades=[]): self.name = name self.grades = grades s1 = Student("Анна") s1.grades.append(5) s2 = Student("Иван") print(s2.grades)
В следующем уроке разберём наследование: как один класс продолжает другой, переиспользуя его атрибуты и методы.
