Атрибуты и специальные методы

В прошлом уроке все данные объекта жили в атрибутах вроде self.name — у каждого объекта свои. Сегодня разберём данные, общие для всех объектов класса, обезвредим классическую ловушку в __init__ и научим объекты печататься и сравниваться по-человечески.

Атрибуты экземпляра и атрибуты класса

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

Такие данные объявляют прямо в теле класса, вне методов. Это атрибут класса:

Python 3.13
class Student:
    school = "Школа №1"      # атрибут класса — один на всех

    def __init__(self, name):
        self.name = name     # атрибут экземпляра — у каждого свой

student1 = Student("Алексей")
student2 = Student("Екатерина")

print(f"{student1.name}, {student1.school}")
Алексей, Школа №1
print(f"{student2.name}, {student2.school}")
Екатерина, Школа №1

Заметьте: student1.school читается, хотя в самом объекте такого атрибута нет. Не найдя имя в экземпляре, Python ищет его в классе. Поэтому достаточно поменять атрибут класса — и изменение увидят сразу все объекты:

Python 3.13
class Student:
    school = "Школа №1"

    def __init__(self, name):
        self.name = name

student1 = Student("Алексей")
student2 = Student("Екатерина")

Student.school = "Школа №5"

print(f"{student1.name}, {student1.school}")
Алексей, Школа №5
print(f"{student2.name}, {student2.school}")
Екатерина, Школа №5
class Student
school = "Школа №1"
student1
name = "Алексей"
school из класса
student2
name = "Екатерина"
school из класса
name — у каждого объекта свой, а school хранится в классе: один на всех

Правило выбора простое: то, что различается между объектами, — в атрибуты экземпляра (имя, возраст), а то, что одно на всех, — в атрибуты класса (константы, общие настройки, значения по умолчанию).

Ловушка с изменяемыми значениями по умолчанию

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

Python 3.13
class 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.13
class 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.13
class 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.13
class 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.13
class Student:
    def __init__(self, name, grades=[]):
        self.name = name
        self.grades = grades

s1 = Student("Анна")
s1.grades.append(5)

s2 = Student("Иван")
print(s2.grades)

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