Розглянемо цю просту проблему:
class Number:
def __init__(self, number):
self.number = number
n1 = Number(1)
n2 = Number(1)
n1 == n2 # False -- oops
Отже, Python за замовчуванням використовує ідентифікатори об'єктів для операцій порівняння:
id(n1) # 140400634555856
id(n2) # 140400634555920
Перевіщення __eq__
функції, здається, вирішує проблему:
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return False
n1 == n2 # True
n1 != n2 # True in Python 2 -- oops, False in Python 3
У Python 2 завжди пам’ятайте про перекриття __ne__
функції, як зазначено в документації :
Між операторами порівняння не маються на увазі зв’язки. Істина x==y
не означає, що x!=y
це помилково. Відповідно, визначаючи __eq__()
, слід також визначитись __ne__()
так, що оператори будуть вести себе так, як очікувалося.
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
return not self.__eq__(other)
n1 == n2 # True
n1 != n2 # False
У Python 3 це більше не потрібно, оскільки в документації зазначено:
За замовчуванням __ne__()
делегує результат __eq__()
та інвертує результат, якщо він не є NotImplemented
. Інших мається на увазі зв’язків між операторами порівняння, наприклад, правда (x<y or x==y)
не означає x<=y
.
Але це не вирішує всіх наших проблем. Додамо підклас:
class SubNumber(Number):
pass
n3 = SubNumber(1)
n1 == n3 # False for classic-style classes -- oops, True for new-style classes
n3 == n1 # True
n1 != n3 # True for classic-style classes -- oops, False for new-style classes
n3 != n1 # False
Примітка: Python 2 має два типи класів:
в класичному стилі (або в старому стилі ) класів, які НЕ успадковуютьobject
і які оголошені якclass A:
,class A():
абоclass A(B):
деB
класкласичному стилі;
класи нового стилю , які успадковуютьobject
і декларуються якclass A(object)
абоclass A(B):
деB
є клас нового стилю. У Python 3 є лише класи нового стилю, які оголошені якclass A:
,class A(object):
абоclass A(B):
.
Для класів класичного стилю операція порівняння завжди називає метод першого операнда, тоді як для класів нового стилю він завжди називає метод операнда підкласу незалежно від порядку операндів .
Тож ось, якщо Number
це клас у класичному стилі:
n1 == n3
дзвінки n1.__eq__
;
n3 == n1
дзвінки n3.__eq__
;
n1 != n3
дзвінки n1.__ne__
;
n3 != n1
дзвінки n3.__ne__
.
А якщо Number
клас нового стилю:
- обидва
n1 == n3
і n3 == n1
дзвоніть n3.__eq__
;
- обидва
n1 != n3
і n3 != n1
дзвоніть n3.__ne__
.
Щоб виправити питання некомутативності ==
та !=
операторів класів класичного стилю Python 2, __eq__
і __ne__
методам слід повернути NotImplemented
значення, коли тип операнду не підтримується. Документація визначає NotImplemented
значення як:
Числові методи та методи багатого порівняння можуть повернути це значення, якщо вони не реалізують операцію для наданих операндів. (Тоді інтерпретатор спробує відобразити операцію чи інший резервний запас, залежно від оператора.) Її значення правдивості є істинним.
В цьому випадку делегати оператора операція порівняння на відображення метод від іншого операнда. У документації визначає відображення методи , як:
Немає версій цих методів з заміненим аргументом (використовуватися, коли лівий аргумент не підтримує операцію, а правий аргумент); швидше, __lt__()
і __gt__()
є відображенням один одного, __le__()
і __ge__()
є відображенням один одного,
__eq__()
і __ne__()
є їх власним відображенням.
Результат виглядає приблизно так:
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return NotImplemented
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
x = self.__eq__(other)
if x is NotImplemented:
return NotImplemented
return not x
Повернення NotImplemented
значення замість False
є правильним , що потрібно зробити , навіть для нових класів , якщо коммутативности з ==
і !=
операторів бажано , коли операнди неспоріднених типів (без успадкування).
Ми там ще? Не зовсім. Скільки унікальних чисел у нас є?
len(set([n1, n2, n3])) # 3 -- oops
Набори використовують хеші об’єктів, і за замовчуванням Python повертає хеш ідентифікатора об'єкта. Спробуємо її перекрити:
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
len(set([n1, n2, n3])) # 1
Кінцевий результат виглядає приблизно так (я додав кілька тверджень в кінці для перевірки):
class Number:
def __init__(self, number):
self.number = number
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return NotImplemented
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
x = self.__eq__(other)
if x is not NotImplemented:
return not x
return NotImplemented
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
class SubNumber(Number):
pass
n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)
assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1
assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1
assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1
assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2
is
оператора, щоб відрізняти ідентичність об'єкта від порівняння значень.