Українська
Порівняння, хешування та контейнери
Рівність, порядок і хешування
__eq__ визначає рівність, __lt__ – відношення «менше». Інші багаті порівняння: __le__, __gt__, __ge__, __ne__. Для невідомого типу повертайте NotImplemented, а не False: інший операнд може знати, як виконати порівняння. Якщо обидва типи відмовилися від рівності, Python має резервну перевірку ідентичності; для впорядкування такої загальної домовленості немає.
Декоратор functools.total_ordering будує відсутні операції порядку з __eq__ і одного методу порядку, зазвичай __lt__. Це зменшує дублювання, але не виправляє помилкову логіку основних методів. Опис: https://docs.python.org/3.14/library/functools.html#functools.total_ordering.
Якщо a == b, їхні хеші мають збігатися. Зворотне твердження неправильне: різні значення можуть мати однаковий хеш. Значення хешу має залишатися сталим протягом перебування об’єкта в множині або словнику. Тому об’єкт, чиї поля рівності можна змінити, не повинен бути ключем. Перевизначення __eq__ у звичайному класі без __hash__ зазвичай робить його нехешованим.
Приклад 2. Гроші як незмінне значення
Сума зберігається в Decimal, валюта – рядковим кодом. Порівняння порядку дозволене лише в одній валюті; автоматичного обміну немає. Decimal створюємо з тексту, а не з float. Цей приклад не потребує округлення: контракт дозволяє довільну скінченну десяткову суму, а подання показує два знаки.
py
from dataclasses import dataclass
from decimal import Decimal
from functools import total_ordering
from types import NotImplementedType
@total_ordering
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str = "UAH"
def __post_init__(self) -> None:
if not self.amount.is_finite():
raise ValueError("нескінченна сума")
if self.currency not in ("UAH", "EUR"):
raise ValueError("невідома валюта")
def __lt__(self, other: object) -> bool | NotImplementedType:
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError("валюти не збігаються")
return self.amount < other.amount
def __str__(self) -> str:
return f"{self.amount:.2f} {self.currency}"
a = Money(Decimal("10.00"))
b = Money(Decimal("12.50"))
print(a, a < b, a <= a)
print(len({a, Money(Decimal("10.0")), b}))10.00 UAH True True
2dataclass створив рівність за двома полями та, завдяки frozen, сумісний хеш. 10.00 і 10.0 рівні як Decimal, тому множина має два елементи. Різні валюти не рівні, навіть якщо суми однакові. Для повноцінного грошового типу окремо визначають масштаб, округлення та допустимі знаки; прихована конвертація в __add__ зробила б просту операцію залежною від зовнішнього курсу.
Контейнерний протокол та виклик об’єкта
__getitem__ одержує індекс або об’єкт slice. У зрізі зберігаються start, stop, step, частина яких може бути None. Для контейнера над списком зручно передати індекс цьому списку: він уже правильно обробляє від’ємні індекси, зрізи та IndexError. Визначте, що повертає зріз: список чи новий екземпляр вашого типу. Послідовність [start:stop] не включає stop.
__contains__ реалізує in; без нього може використовуватися ітерація. __iter__ повинен повернути ітератор, а не довільний список. Виклик iter(self._items) дає незалежний обхід для кожного циклу. Якщо сам контейнер повертає self і тримає єдиний індекс, вкладені цикли можуть заважати один одному. Для окремого ітератора потрібен __next__, який після завершення піднімає StopIteration.
__setitem__ і __delitem__ потрібні лише змінюваному контейнеру. Наявність читання не зобов’язує підтримувати запис. Наприклад, архів результатів може дозволяти індекси та перебір, але забороняти видалення. Не повертайте внутрішній змінюваний список напряму, якщо зовнішній код не повинен обходити перевірки контейнера.
Викличний об’єкт реалізує __call__. Він зручний, коли разом із правилом обчислення потрібно зберігати налаштування. Наприклад, екземпляр лінійної функції тримає коефіцієнти та обчислює значення.
py
from dataclasses import dataclass
@dataclass(frozen=True)
class Linear:
a: float
b: float
def __call__(self, x: float) -> float:
return self.a * x + self.b
rule = Linear(2, 3)
print(rule(4), rule(-1))11 1Операція має залишатися змістовною. Метод __call__ не повинен несподівано видаляти файл або завершувати процес, якщо об’єкт виглядає як математична функція. Узгодженість із очікуваннями користувача є частиною якості інтерфейсу.