31. Продвинутые возможности классов
В главе 30 мы научились создавать базовые классы с атрибутами и методами экземпляра. Теперь мы рассмотрим более сложные возможности классов, которые дают вам тонкий контроль над тем, как ведут себя ваши объекты. Эти возможности позволяют создавать классы, которые ощущаются как встроенные типы Python, с естественным синтаксисом для операций вроде сложения, сравнения и индексации.
31.1) Переменные класса vs переменные экземпляра
Когда мы создаём атрибуты в классе, у нас есть два принципиально разных места, где их можно хранить: в самом классе или в отдельных экземплярах. Понимание этого различия критично для написания корректного объектно-ориентированного кода.
31.1.1) Понимание переменных экземпляра
Переменные экземпляра (instance variables) — это атрибуты, которые принадлежат конкретному объекту. У каждого экземпляра есть собственная отдельная копия этих переменных. Мы использовали переменные экземпляра на протяжении всей главы 30 — это атрибуты, которые мы создаём в __init__, используя self:
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) — это атрибуты, которые принадлежат самому классу, а не какому-то конкретному экземпляру. Все экземпляры разделяют одну и ту же переменную класса. Мы определяем переменные класса прямо в теле класса, вне любых методов:
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). Оба варианта ссылаются на одну и ту же переменную.
Вот что делает переменные класса мощным инструментом — когда мы изменяем переменную класса, все экземпляры видят это изменение:
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 создаёт переменную экземпляра, которая затеняет (скрывает) переменную класса:
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) Практические применения переменных класса
Переменные класса полезны для данных, которые должны быть общими для всех экземпляров:
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), чтобы было ясно: мы модифицируем переменную класса, а не создаём переменную экземпляра.
31.2) Управление атрибутами с помощью @property
Иногда вы хотите контролировать, что происходит, когда кто-то обращается к атрибуту или изменяет его. Например, вам может понадобиться проверять, что значение положительное, или вычислять значение «на лету», вместо того чтобы хранить его. Декоратор @property в Python позволяет писать методы, которые выглядят как простой доступ к атрибуту.
31.2.1) Проблема: прямой доступ к атрибутам не позволяет валидировать
Когда к атрибутам обращаются напрямую, вы не можете валидировать или преобразовывать значения:
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)», к которому обращаются как к атрибуту:
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:
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:
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) Использование свойств для валидации
Свойства отлично подходят для принудительного соблюдения ограничений:
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) Свойства только для чтения
Если вы определяете свойство без сеттера, оно становится доступным только для чтения:
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Это полезно для производных значений, которые должны вычисляться, а не храниться.
31.3) Методы класса с @classmethod
Иногда вам нужны методы, которые работают с самим классом, а не с экземплярами. Методы класса (class methods) получают класс в качестве первого аргумента (по соглашению он называется cls) вместо экземпляра (self).
31.3.1) Определение методов класса
Мы создаём методы класса с помощью декоратора @classmethod:
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) Альтернативные конструкторы с методами класса
Одно из самых распространённых применений методов класса — создание альтернативных конструкторов: разных способов создавать экземпляры:
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) Методы класса для фабричных шаблонов
Методы класса полезны для создания экземпляров с разными конфигурациями:
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:543231.3.4) Методы класса для подсчёта экземпляров
Методы класса могут работать с переменными класса, чтобы отслеживать информацию обо всех экземплярах:
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: 031.4) Статические методы с @staticmethod
Статические методы (static methods) — это методы, которые не получают ни экземпляр (self), ни класс (cls) в качестве первого аргумента. Это просто обычные функции, которые оказались определены внутри класса, потому что логически связаны с этим классом.
31.4.1) Определение статических методов
Мы создаём статические методы с помощью декоратора @staticmethod:
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 методы экземпляра
Вот как выбрать:
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) Практический пример: утилиты для валидации
Статические методы отлично подходят для валидации и вспомогательных функций:
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 username31.5) Понимание специальных методов (магических методов)
Специальные методы (special methods) (их также называют магическими методами (magic methods) или dunder-методами (dunder methods), потому что у них двойные подчёркивания) позволяют настраивать, как ваши объекты ведут себя во встроенных операциях Python. Мы уже использовали __init__, __str__ и __repr__ в главе 30. Теперь мы рассмотрим гораздо больше.
31.5.1) Что делают специальные методы
Специальные методы автоматически вызываются 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() для вашего объекта:
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:
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) Построение более полного класса коллекции
Давайте создадим более реалистичный класс коллекции, который отслеживает оценки учеников:
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__() вызывается, когда вы используете оператор +:
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__() вызывается, когда вы используете оператор ==:
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:
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: True31.7.4) Обработка несоответствия типов в операторах
При реализации операторов стоит обрабатывать случаи, когда другой операнд имеет неожиданный тип:
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 попробовать отражённую операцию на другом операнде. Это важно, чтобы операторы корректно работали с разными типами.
31.8) Пример 3: доступ как у последовательностей (getitem, setitem)
Специальные методы __getitem__() и __setitem__() позволяют использовать синтаксис индексации (obj[key]) с вашими пользовательскими классами. Это заставляет ваши объекты вести себя как списки, словари или другие последовательности.
31.8.1) Реализация getitem для индексации
Метод __getitem__() вызывается, когда вы используете квадратные скобки для доступа к элементу:
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__() также обрабатывает срезы:
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__() вызывается, когда вы присваиваете значение по индексу:
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 B31.8.4) Как сделать объект итерируемым с помощью getitem
Интересный побочный эффект: если вы реализуете __getitem__() с целочисленными индексами, начиная с 0, ваш объект автоматически становится итерируемым:
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 CPython пытается итерироваться, вызывая __getitem__(0), затем __getitem__(1) и так далее, пока не получит IndexError. Это более старый протокол итерации — о современном протоколе итераторов мы узнаем в главе 35.
31.8.5) Доступ как у словаря со строковыми ключами
__getitem__() и __setitem__() работают с любым типом ключа, не только с целыми числами:
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Эта глава показала вам, как создавать сложные классы, которые бесшовно интегрируются с синтаксисом Python. Реализуя переменные класса, свойства, методы класса, статические методы и специальные методы, вы можете сделать так, чтобы ваши пользовательские классы вели себя как встроенные типы. В главе 32 мы изучим наследование и полиморфизм, которые позволяют строить иерархии связанных классов, разделяющих и расширяющих поведение.