Python & AI Tutorials Logo
Программирование Python

31. Продвинутые возможности классов

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

31.1) Переменные класса vs переменные экземпляра

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

31.1.1) Понимание переменных экземпляра

Переменные экземпляра (instance variables) — это атрибуты, которые принадлежат конкретному объекту. У каждого экземпляра есть собственная отдельная копия этих переменных. Мы использовали переменные экземпляра на протяжении всей главы 30 — это атрибуты, которые мы создаём в __init__, используя self:

python
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner      # Переменная экземпляра
        self.balance = balance  # Переменная экземпляра
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.balance)  # Output: 1000
print(account2.balance)  # Output: 500

У каждого экземпляра BankAccount есть свои собственные owner и balance. Изменение account1.balance не влияет на account2.balance — они полностью независимы.

31.1.2) Понимание переменных класса

Переменные класса (class variables) — это атрибуты, которые принадлежат самому классу, а не какому-то конкретному экземпляру. Все экземпляры разделяют одну и ту же переменную класса. Мы определяем переменные класса прямо в теле класса, вне любых методов:

python
class BankAccount:
    interest_rate = 0.02  # Переменная класса — общая для всех экземпляров
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
    
    def apply_interest(self):
        self.balance += self.balance * BankAccount.interest_rate
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.interest_rate)  # Output: 0.02
print(account2.interest_rate)  # Output: 0.02
print(BankAccount.interest_rate)  # Output: 0.02

Обратите внимание, что мы можем обращаться к interest_rate через экземпляры (account1.interest_rate) или через сам класс (BankAccount.interest_rate). Оба варианта ссылаются на одну и ту же переменную.

Вот что делает переменные класса мощным инструментом — когда мы изменяем переменную класса, все экземпляры видят это изменение:

python
class BankAccount:
    interest_rate = 0.02
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.interest_rate)  # Output: 0.02
print(account2.interest_rate)  # Output: 0.02
 
# Изменяем переменную класса
BankAccount.interest_rate = 0.03
 
print(account1.interest_rate)  # Output: 0.03
print(account2.interest_rate)  # Output: 0.03

Оба экземпляра сразу видят новую процентную ставку, потому что все они смотрят на одну и ту же переменную класса.

31.1.3) Ловушка затенения: когда переменные экземпляра скрывают переменные класса

Вот тонкое, но важное поведение: если вы присваиваете значение атрибуту через экземпляр, Python создаёт переменную экземпляра, которая затеняет (скрывает) переменную класса:

python
class BankAccount:
    interest_rate = 0.02
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
# Создаём переменную экземпляра, которая затеняет переменную класса
account1.interest_rate = 0.05
 
print(account1.interest_rate)  # Output: 0.05 (instance variable)
print(account2.interest_rate)  # Output: 0.02 (class variable)
print(BankAccount.interest_rate)  # Output: 0.02 (class variable)

Теперь у account1 есть собственная переменная экземпляра interest_rate, которая скрывает переменную класса. Переменная класса всё ещё существует, но account1.interest_rate ссылается на переменную экземпляра. Обычно это не то, что вам нужно — если требуется изменить переменную класса, изменяйте её через имя класса, а не через экземпляр.

31.1.4) Практические применения переменных класса

Переменные класса полезны для данных, которые должны быть общими для всех экземпляров:

python
class Student:
    school_name = "Python High School"  # Одинаково для всех учеников
    total_students = 0  # Отслеживаем, сколько учеников существует
    
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
        Student.total_students += 1  # Увеличиваем при создании ученика
    
    def __str__(self):
        return f"{self.name} (Grade {self.grade}) at {Student.school_name}"
 
student1 = Student("Alice", 10)
student2 = Student("Bob", 11)
student3 = Student("Carol", 10)
 
print(student1)  # Output: Alice (Grade 10) at Python High School
print(f"Total students: {Student.total_students}")  # Output: Total students: 3

Обратите внимание, что в __init__ мы используем Student.total_students (а не self.total_students), чтобы было ясно: мы модифицируем переменную класса, а не создаём переменную экземпляра.

Переменные класса

Определены в теле класса

Общие для всех экземпляров

Доступ через ClassName.variable

Переменные экземпляра

Определены в init через self

Уникальны для каждого экземпляра

Доступ через instance.variable

31.2) Управление атрибутами с помощью @property

Иногда вы хотите контролировать, что происходит, когда кто-то обращается к атрибуту или изменяет его. Например, вам может понадобиться проверять, что значение положительное, или вычислять значение «на лету», вместо того чтобы хранить его. Декоратор @property в Python позволяет писать методы, которые выглядят как простой доступ к атрибуту.

31.2.1) Проблема: прямой доступ к атрибутам не позволяет валидировать

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

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
 
temp = Temperature(25)
print(temp.celsius)  # Output: 25
 
# Ничто не мешает нам задавать физически невозможные температуры
temp.celsius = -500  # Ниже абсолютного нуля (-273.15°C)!
print(temp.celsius)  # Output: -500
 
# Или абсурдно большие значения
temp.celsius = 1000000
print(temp.celsius)  # Output: 1000000

Без валидации мы можем случайно установить некорректные данные, что приведёт к багам позже в программе. Мы могли бы использовать методы вроде get_celsius() и set_celsius(), но это не идиоматично для Python. Разработчики Python ожидают обращаться к атрибутам напрямую, а не через методы getter/setter, как в Java или C++.

31.2.2) Использование @property для вычисляемых атрибутов

Декоратор @property превращает метод в «геттер (getter)», к которому обращаются как к атрибуту:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    @property
    def fahrenheit(self):
        """Преобразовать celsius в fahrenheit на лету"""
        return self.celsius * 9/5 + 32
 
temp = Temperature(25)
print(temp.celsius)  # Output: 25
print(temp.fahrenheit)  # Output: 77.0 (computed, not stored)

Обратите внимание: мы обращаемся к temp.fahrenheit без скобок — это выглядит как доступ к атрибуту, но на самом деле вызывается метод. Значение по Фаренгейту вычисляется каждый раз при обращении, поэтому оно всегда синхронизировано с celsius:

python
temp = Temperature(0)
print(temp.fahrenheit)  # Output: 32.0
 
temp.celsius = 100
print(temp.fahrenheit)  # Output: 212.0 (automatically updated)

31.2.3) Добавление сеттера с @property_name.setter

Чтобы разрешить установку значения свойства(property), мы добавляем метод-сеттер с помощью декоратора @property_name.setter:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    @property
    def fahrenheit(self):
        return self.celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        """Преобразовать fahrenheit в celsius при установке"""
        self.celsius = (value - 32) * 5/9
 
temp = Temperature(0)
print(temp.celsius)  # Output: 0
print(temp.fahrenheit)  # Output: 32.0
 
# Устанавливаем температуру через fahrenheit
temp.fahrenheit = 212
print(temp.celsius)  # Output: 100.0
print(temp.fahrenheit)  # Output: 212.0

Метод-сеттер получает новое значение и может проверить его или преобразовать перед сохранением.

31.2.4) Использование свойств для валидации

Свойства отлично подходят для принудительного соблюдения ограничений:

python
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self._balance = balance  # Подчёркивание намекает на «для внутреннего использования»
    
    @property
    def balance(self):
        """Получить текущий баланс"""
        return self._balance
    
    @balance.setter
    def balance(self, value):
        """Установить баланс, но только если он неотрицательный"""
        if value < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = value
 
account = BankAccount("Alice", 1000)
print(account.balance)  # Output: 1000
 
account.balance = 1500  # Работает нормально
print(account.balance)  # Output: 1500
 
# Это вызовет ошибку
account.balance = -100
# Output: ValueError: Balance cannot be negative

Обратите внимание на соглашение об именовании: фактическое значение мы храним в _balance (с ведущим подчёркиванием) и открываем доступ к нему через свойство balance. Подчёркивание — это соглашение Python, которое означает «это внутренняя деталь реализации», хотя атрибут технически всё равно доступен. Этот шаблон позволяет контролировать доступ через свойство, при этом отделяя фактическое хранилище значения.

31.2.5) Свойства только для чтения

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

python
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    @property
    def area(self):
        """Вычисляемое свойство только для чтения"""
        return self.width * self.height
 
rect = Rectangle(5, 3)
print(rect.area)  # Output: 15
 
rect.width = 10
print(rect.area)  # Output: 30 (automatically updated)
 
# Попытка установить area вызовет ошибку
rect.area = 50
# Output: AttributeError: property 'area' of 'Rectangle' object has no setter

Это полезно для производных значений, которые должны вычисляться, а не храниться.

Декоратор @property

Превращает метод в геттер

Доступ как к атрибуту

Может вычислять значение на лету

@property_name.setter

Добавляет сеттер для свойства

Может валидировать перед сохранением

Может преобразовывать значение

31.3) Методы класса с @classmethod

Иногда вам нужны методы, которые работают с самим классом, а не с экземплярами. Методы класса (class methods) получают класс в качестве первого аргумента (по соглашению он называется cls) вместо экземпляра (self).

31.3.1) Определение методов класса

Мы создаём методы класса с помощью декоратора @classmethod:

python
class Student:
    school_name = "Python High School"
    
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
    
    @classmethod
    def get_school_name(cls):
        """Метод класса — получает класс, а не экземпляр"""
        return cls.school_name
 
# Вызываем на самом классе
print(Student.get_school_name())  # Output: Python High School
 
# Можно вызывать и на экземпляре (но cls всё равно будет классом)
student = Student("Alice", 10)
print(student.get_school_name())  # Output: Python High School

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

31.3.2) Альтернативные конструкторы с методами класса

Одно из самых распространённых применений методов класса — создание альтернативных конструкторов: разных способов создавать экземпляры:

python
class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    
    @classmethod
    def from_string(cls, date_string):
        """Создать Date из строки вида '2024-12-27'"""
        year, month, day = date_string.split('-')
        return cls(int(year), int(month), int(day))
    
    @classmethod
    def today(cls):
        """Создать Date для сегодняшнего дня (упрощённый пример)"""
        # В реальном коде вы бы использовали модуль datetime
        return cls(2024, 12, 27)
    
    def __str__(self):
        return f"{self.year}-{self.month:02d}-{self.day:02d}"
 
# Обычный конструктор
date1 = Date(2024, 12, 27)
print(date1)  # Output: 2024-12-27
 
# Альтернативный конструктор из строки
date2 = Date.from_string("2024-12-27")
print(date2)  # Output: 2024-12-27
 
# Альтернативный конструктор для текущей даты
date3 = Date.today()
print(date3)  # Output: 2024-12-27

Обратите внимание: и from_string, и today возвращают cls(...) — это создаёт новый экземпляр класса. Использование cls вместо жёстко заданного Date делает код корректным для подклассов (мы изучим наследование в главе 32).

31.3.3) Методы класса для фабричных шаблонов

Методы класса полезны для создания экземпляров с разными конфигурациями:

python
class DatabaseConnection:
    def __init__(self, host, port, database, username):
        self.host = host
        self.port = port
        self.database = database
        self.username = username
    
    @classmethod
    def for_development(cls):
        """Создать соединение, настроенное для разработки"""
        return cls("localhost", 5432, "dev_db", "dev_user")
    
    @classmethod
    def for_production(cls):
        """Создать соединение, настроенное для продакшена"""
        return cls("prod.example.com", 5432, "prod_db", "prod_user")
    
    def __str__(self):
        return f"Connection to {self.database} at {self.host}:{self.port}"
 
# Легко создавать заранее настроенные соединения
dev_conn = DatabaseConnection.for_development()
prod_conn = DatabaseConnection.for_production()
 
print(dev_conn)  # Output: Connection to dev_db at localhost:5432
print(prod_conn)  # Output: Connection to prod_db at prod.example.com:5432

31.3.4) Методы класса для подсчёта экземпляров

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

python
class Product:
    total_products = 0
    
    def __init__(self, name, price):
        self.name = name
        self.price = price
        Product.total_products += 1
    
    @classmethod
    def get_total_products(cls):
        """Вернуть общее количество созданных продуктов"""
        return cls.total_products
    
    @classmethod
    def reset_count(cls):
        """Сбросить счётчик продуктов"""
        cls.total_products = 0
 
product1 = Product("Laptop", 999)
product2 = Product("Mouse", 25)
product3 = Product("Keyboard", 75)
 
print(Product.get_total_products())  # Output: 3
 
Product.reset_count()
print(Product.get_total_products())  # Output: 0

31.4) Статические методы с @staticmethod

Статические методы (static methods) — это методы, которые не получают ни экземпляр (self), ни класс (cls) в качестве первого аргумента. Это просто обычные функции, которые оказались определены внутри класса, потому что логически связаны с этим классом.

31.4.1) Определение статических методов

Мы создаём статические методы с помощью декоратора @staticmethod:

python
class MathUtils:
    @staticmethod
    def is_even(number):
        """Проверить, является ли число чётным"""
        return number % 2 == 0
    
    @staticmethod
    def is_prime(number):
        """Проверить, является ли число простым (упрощённо)"""
        if number < 2:
            return False
        for i in range(2, int(number ** 0.5) + 1):
            if number % i == 0:
                return False
        return True
 
# Вызываем статические методы на классе
print(MathUtils.is_even(4))  # Output: True
print(MathUtils.is_even(7))  # Output: False
print(MathUtils.is_prime(17))  # Output: True
print(MathUtils.is_prime(18))  # Output: False
 
# Можно вызывать и на экземпляре (но это та же самая функция)
utils = MathUtils()
print(utils.is_even(10))  # Output: True

Статические методы не нуждаются в доступе к данным экземпляра или класса — это самодостаточные вспомогательные функции.

31.4.2) Когда использовать статические методы vs методы класса vs методы экземпляра

Вот как выбрать:

python
class Temperature:
    # Переменная класса
    absolute_zero_celsius = -273.15
    
    def __init__(self, celsius):
        self.celsius = celsius
    
    # Метод экземпляра — нужен доступ к данным экземпляра (self)
    def to_fahrenheit(self):
        return self.celsius * 9/5 + 32
    
    # Метод класса — нужен доступ к данным класса (cls)
    @classmethod
    def get_absolute_zero(cls):
        return cls.absolute_zero_celsius
    
    # Статический метод — не нужны данные ни экземпляра, ни класса
    @staticmethod
    def celsius_to_kelvin(celsius):
        return celsius + 273.15
    
    @staticmethod
    def fahrenheit_to_celsius(fahrenheit):
        return (fahrenheit - 32) * 5/9
 
temp = Temperature(25)
 
# Метод экземпляра — использует данные экземпляра
print(temp.to_fahrenheit())  # Output: 77.0
 
# Метод класса — использует данные класса
print(Temperature.get_absolute_zero())  # Output: -273.15
 
# Статические методы — просто вспомогательные функции
print(Temperature.celsius_to_kelvin(25))  # Output: 298.15
print(Temperature.fahrenheit_to_celsius(77))  # Output: 25.0

Рекомендации:

  • Используйте методы экземпляра (instance methods), когда нужен доступ к атрибутам экземпляра (self)
  • Используйте методы класса (class methods), когда нужен доступ к атрибутам класса или нужны альтернативные конструкторы (cls)
  • Используйте статические методы (static methods), когда не нужен доступ к данным экземпляра или класса, но функция логически связана с классом

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

Тип методаПервый параметрКогда использовать
Метод экземпляраselfНужен доступ к данным экземпляра
Метод классаclsНужен доступ к данным класса или альтернативные конструкторы
Статический метод(нет)Вспомогательная функция, связанная с классом

31.4.3) Практический пример: утилиты для валидации

Статические методы отлично подходят для валидации и вспомогательных функций:

python
class User:
    def __init__(self, username, password):
        if not User.is_valid_username(username):
            raise ValueError("Invalid username")
        if not User.is_valid_password(password):
            raise ValueError("Invalid password")
        
        self.username = username
        self._password = password
    
    @staticmethod
    def is_valid_username(username):
        """Проверить, соответствует ли username требованиям"""
        return len(username) >= 3 and username.isalnum()
        
    @staticmethod
    def is_valid_password(password):
        """Проверить, соответствует ли password требованиям безопасности"""
        return len(password) >= 8 and any(c.isdigit() for c in password)
 
# Эти методы валидации можно использовать независимо
print(User.is_valid_username("alice123"))  # Output: True
print(User.is_valid_username("ab"))  # Output: False
print(User.is_valid_password("pass1234"))  # Output: True
 
# И их можно использовать в любом методе класса
try:
    user = User("ab", "short")
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: Invalid username

31.5) Понимание специальных методов (магических методов)

Специальные методы (special methods) (их также называют магическими методами (magic methods) или dunder-методами (dunder methods), потому что у них двойные подчёркивания) позволяют настраивать, как ваши объекты ведут себя во встроенных операциях Python. Мы уже использовали __init__, __str__ и __repr__ в главе 30. Теперь мы рассмотрим гораздо больше.

31.5.1) Что делают специальные методы

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

python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Point({self.x}, {self.y})"
 
point = Point(3, 4)
 
# Когда вы вызываете print(), Python вызывает __str__()
print(point)  # Output: Point(3, 4)
# This is equivalent to: print(point.__str__())

Специальные методы позволяют сделать так, чтобы ваши классы вели себя как встроенные типы. Например, вы можете сделать так, чтобы ваши объекты:

  • Поддерживали арифметические операции (+, -, *, /)
  • Были сравнимы (<, >, ==)
  • Работали с len(), in и индексацией
  • Вели себя как контейнеры или последовательности

Мы подробно рассмотрим их в следующих разделах.

31.5.2) Распространённые категории специальных методов

Вот основные категории специальных методов:

Строковое представление (как отображаются объекты):

  • __str__() — для print() и str()
  • __repr__() — для REPL и repr()

Сравнение (сравнение объектов):

  • __eq__() — для ==
  • __ne__() — для !=
  • __lt__() — для <
  • __le__() — для <=
  • __gt__() — для >
  • __ge__() — для >=

Арифметика (математические операции):

  • __add__() — для +
  • __sub__() — для -
  • __mul__() — для *
  • __truediv__() — для /

Контейнер/последовательность (поведение как у коллекций):

  • __len__() — для len()
  • __contains__() — для in
  • __getitem__() — для индексации obj[key]
  • __setitem__() — для присваивания obj[key] = value

Мы подробно разберём их в следующих разделах.

31.6) Пример 1: интерфейс коллекции (len, contains)

Давайте создадим класс, который управляет коллекцией элементов, и сделаем так, чтобы он работал со встроенной функцией len() и оператором in.

31.6.1) Реализация len для len()

Специальный метод __len__() вызывается, когда вы используете len() для вашего объекта:

python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def __len__(self):
        """Вернуть количество элементов в корзине"""
        return len(self.items)
 
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
cart.add_item("Orange")
 
# len() вызывает __len__()
print(len(cart))  # Output: 3

Без __len__() вызов len(cart) привёл бы к TypeError. Реализовав его, мы делаем так, что ShoppingCart работает так же, как встроенные коллекции.

31.6.2) Реализация contains для оператора in

Специальный метод __contains__() вызывается, когда вы используете оператор in:

python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def __len__(self):
        return len(self.items)
    
    def __contains__(self, item):
        """Проверить, находится ли элемент в корзине"""
        return item in self.items
 
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
 
# Оператор in вызывает __contains__()
print("Apple" in cart)  # Output: True
print("Orange" in cart)  # Output: False

Теперь наша корзина поддерживает естественный синтаксис Python для проверки принадлежности.

31.6.3) Построение более полного класса коллекции

Давайте создадим более реалистичный класс коллекции, который отслеживает оценки учеников:

python
class GradeBook:
    def __init__(self):
        self.grades = {}  # student_name: list of grades
    
    def add_grade(self, student, grade):
        """Добавить оценку ученику"""
        if student not in self.grades:
            self.grades[student] = []
        self.grades[student].append(grade)
    
    def __len__(self):
        """Вернуть количество учеников"""
        return len(self.grades)
    
    def __contains__(self, student):
        """Проверить, есть ли у ученика хоть какие-то оценки"""
        return student in self.grades
    
    def get_average(self, student):
        """Получить среднюю оценку ученика"""
        if student not in self:
            return None
        grades = self.grades[student]
        return sum(grades) / len(grades)
    
    def __str__(self):
        return f"GradeBook with {len(self)} students"
 
gradebook = GradeBook()
gradebook.add_grade("Alice", 85)
gradebook.add_grade("Alice", 90)
gradebook.add_grade("Bob", 78)
gradebook.add_grade("Bob", 82)
gradebook.add_grade("Bob", 88)
 
print(gradebook)  # Output: GradeBook with 2 students
print(len(gradebook))  # Output: 2
 
print("Alice" in gradebook)  # Output: True
print("Carol" in gradebook)  # Output: False
 
print(f"Alice's average: {gradebook.get_average('Alice')}")  # Output: Alice's average: 87.5
print(f"Bob's average: {gradebook.get_average('Bob')}")  # Output: Bob's average: 82.66666666666667

Обратите внимание, как get_average() использует if student not in self — это вызывает наш метод __contains__(), делая код естественно читаемым.

31.7) Пример 2: перегрузка операторов (add, eq, lt)

Перегрузка операторов (operator overloading) означает определение того, что делают операторы вроде +, == и < для ваших пользовательских классов. Это позволяет вашим объектам естественно работать с синтаксисом Python.

31.7.1) Реализация add для сложения

Специальный метод __add__() вызывается, когда вы используете оператор +:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """Сложить два вектора"""
        return Vector(self.x + other.x, self.y + other.y)
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(3, 4)
 
# Оператор + вызывает __add__()
v3 = v1 + v2
print(v3)  # Output: Vector(4, 6)

Когда Python видит v1 + v2, он вызывает v1.__add__(v2). Метод __add__() левого операнда получает правый операнд в качестве аргумента.

31.7.2) Реализация eq для равенства

Специальный метод __eq__() вызывается, когда вы используете оператор ==:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = 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
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(1, 2)
v3 = Vector(3, 4)
 
# Оператор == вызывает __eq__()
print(v1 == v2)  # Output: True
print(v1 == v3)  # Output: False

Без __eq__() Python сравнивает идентичность объектов (являются ли они одним и тем же объектом в памяти), а не их значения. С __eq__() мы определяем, что означает равенство для нашего класса.

31.7.3) Реализация операторов сравнения

Давайте реализуем операторы сравнения для класса Money:

python
class Money:
    def __init__(self, amount):
        self.amount = amount
    
    def __eq__(self, other):
        """Проверить, равны ли суммы"""
        return self.amount == other.amount
    
    def __lt__(self, other):
        """Проверить, меньше ли эта сумма, чем другая"""
        return self.amount < other.amount
    
    def __le__(self, other):
        """Проверить, меньше ли эта сумма или равна другой"""
        return self.amount <= other.amount
    
    def __gt__(self, other):
        """Проверить, больше ли эта сумма, чем другая"""
        return self.amount > other.amount
    
    def __ge__(self, other):
        """Проверить, больше ли эта сумма или равна другой"""
        return self.amount >= other.amount
    
    def __str__(self):
        return f"${self.amount:.2f}"
 
price1 = Money(10.50)
price2 = Money(15.75)
price3 = Money(10.50)
 
print(price1 == price3)  # Output: True
print(price1 < price2)  # Output: True
print(price1 <= price3)  # Output: True
print(price2 > price1)  # Output: True
print(price2 >= price1)  # Output: True

31.7.4) Обработка несоответствия типов в операторах

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

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """Сложить два вектора или прибавить скаляр к обеим компонентам"""
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        elif isinstance(other, (int, float)):
            return Vector(self.x + other, self.y + other)
        else:
            return NotImplemented  # Пусть Python попробует other.__radd__(self)
    
    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(3, 4)
 
print(v1 + v2)  # Output: Vector(4, 6) (vector addition)
print(v1 + 5)  # Output: Vector(6, 7) (scalar addition)
 
print(v1 == v2)  # Output: False
print(v1 == "not a vector")  # Output: False (no error)

Возврат NotImplemented (специальной встроенной константы) сообщает Python попробовать отражённую операцию на другом операнде. Это важно, чтобы операторы корректно работали с разными типами.

Перегрузка операторов

Арифметические операторы

Операторы сравнения

add для +

sub для -

mul для *

truediv для /

eq для ==

lt для <

le для <=

gt для >

ge для >=

31.8) Пример 3: доступ как у последовательностей (getitem, setitem)

Специальные методы __getitem__() и __setitem__() позволяют использовать синтаксис индексации (obj[key]) с вашими пользовательскими классами. Это заставляет ваши объекты вести себя как списки, словари или другие последовательности.

31.8.1) Реализация getitem для индексации

Метод __getitem__() вызывается, когда вы используете квадратные скобки для доступа к элементу:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        """Получить песню по индексу"""
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
# Индексация вызывает __getitem__()
print(playlist[0])  # Output: Song A
print(playlist[1])  # Output: Song B
print(playlist[-1])  # Output: Song C (negative indexing works!)

Поскольку мы делегируем операцию в self.songs[index], все возможности индексации списка работают автоматически: положительные индексы, отрицательные индексы и даже выбрасывание IndexError для недопустимых индексов.

31.8.2) Поддержка срезов с getitem

Тот же метод __getitem__() также обрабатывает срезы:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        """Получить песню по индексу или срез"""
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
playlist.add_song("Song D")
 
# Срезы тоже вызывают __getitem__()
print(playlist[1:3])  # Output: ['Song B', 'Song C']
print(playlist[:2])  # Output: ['Song A', 'Song B']
print(playlist[::2])  # Output: ['Song A', 'Song C']

Когда вы используете срез, Python передаёт в __getitem__() объект slice. За счёт делегирования в self.songs[index] мы автоматически поддерживаем весь синтаксис срезов.

31.8.3) Реализация setitem для присваивания

Метод __setitem__() вызывается, когда вы присваиваете значение по индексу:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        return self.songs[index]
    
    def __setitem__(self, index, value):
        """Заменить песню на конкретном индексе"""
        self.songs[index] = value
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
print(playlist[1])  # Output: Song B
 
# Присваивание вызывает __setitem__()
playlist[1] = "New Song B"
print(playlist[1])  # Output: New Song B

31.8.4) Как сделать объект итерируемым с помощью getitem

Интересный побочный эффект: если вы реализуете __getitem__() с целочисленными индексами, начиная с 0, ваш объект автоматически становится итерируемым:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
# Циклы for работают автоматически!
for song in playlist:
    print(song)
# Output:
# Song A
# Song B
# Song C

Python пытается итерироваться, вызывая __getitem__(0), затем __getitem__(1) и так далее, пока не получит IndexError. Это более старый протокол итерации — о современном протоколе итераторов мы узнаем в главе 35.

31.8.5) Доступ как у словаря со строковыми ключами

__getitem__() и __setitem__() работают с любым типом ключа, не только с целыми числами:

python
class ScoreBoard:
    def __init__(self):
        self.scores = {}
    
    def __getitem__(self, player_name):
        """Получить счёт игрока"""
        return self.scores.get(player_name, 0)
    
    def __setitem__(self, player_name, score):
        """Установить счёт игрока"""
        self.scores[player_name] = score
    
    def __contains__(self, player_name):
        return player_name in self.scores
    
    def __len__(self):
        return len(self.scores)
 
scoreboard = ScoreBoard()
 
# Устанавливаем счёт через строковые ключи
scoreboard["Alice"] = 100
scoreboard["Bob"] = 85
 
# Обновляем счёт
scoreboard["Alice"] = 120
 
# Получаем счёт
print(scoreboard["Alice"])  # Output: 120
print(scoreboard["Bob"])    # Output: 85
print(scoreboard["Carol"])  # Output: 0
 
print("Alice" in scoreboard)  # Output: True
print(len(scoreboard))  # Output: 2

Доступ как у последовательностей

getitem

setitem

Вызывается для obj[key]

Обрабатывает индексацию

Обрабатывает срезы

Делает объект итерируемым

Вызывается для obj[key] = value

Разрешает присваивание

Может валидировать значения


Эта глава показала вам, как создавать сложные классы, которые бесшовно интегрируются с синтаксисом Python. Реализуя переменные класса, свойства, методы класса, статические методы и специальные методы, вы можете сделать так, чтобы ваши пользовательские классы вели себя как встроенные типы. В главе 32 мы изучим наследование и полиморфизм, которые позволяют строить иерархии связанных классов, разделяющих и расширяющих поведение.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai