Eleganckie sposoby wspierania równoważności ("equality") w klasach Pythona
Podczas pisania klas niestandardowych często ważne jest, aby zezwolić na równoważność za pomocą operatorów ==
i !=
. W Pythonie jest to możliwe dzięki implementacji, odpowiednio, specjalnych metod __eq__
i __ne__
. Najprostszym sposobem, jaki znalazłem, jest następująca metoda:
class Foo:
def __init__(self, item):
self.item = item
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
Czy znasz bardziej eleganckie sposoby, aby to zrobić? Czy znasz jakieś szczególne wady stosowania powyższej metody porównywania __dict__
s?
Uwaga : trochę Wyjaśnienie--gdy __eq__
i __ne__
są niezdefiniowane, znajdziesz takie zachowanie:
>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False
Czyli a == b
ocenia na False
, ponieważ naprawdę działa a is b
, test tożsamości (tj. "czy a
jest tym samym obiektem co b
?").
Kiedy __eq__
i __ne__
są zdefiniowane, znajdziesz takie zachowanie (którego szukamy):
>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True
8 answers
Rozważ ten prosty problem:
class Number:
def __init__(self, number):
self.number = number
n1 = Number(1)
n2 = Number(1)
n1 == n2 # False -- oops
Tak więc Python domyślnie używa identyfikatorów obiektów do operacji porównywania:
id(n1) # 140400634555856
id(n2) # 140400634555920
Nadpisanie funkcji __eq__
wydaje się rozwiązywać problem:
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
W Python 2 zawsze pamiętaj, aby nadpisać funkcję __ne__
, ponieważ dokumentacja stwierdza:
Między operatorami porównania nie istnieją żadne implikowane relacje. Na prawda
x==y
nie oznacza, żex!=y
jest fałsz. Odpowiednio, gdy definiowanie__eq__()
, należy również zdefiniować__ne__()
, aby operatorzy będą zachowywać się zgodnie z oczekiwaniami.
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
return not self.__eq__(other)
n1 == n2 # True
n1 != n2 # False
W Python 3 , nie jest to już konieczne, ponieważ dokumentacja stwierdza:
Ale to nie rozwiązuje wszystkich naszych problemów. Dodajmy podklasę:Domyślnie,
__ne__()
deleguje na__eq__()
i odwraca wynik chyba że jestNotImplemented
. Nie ma innych domniemanych zależności między operatorami porównania, np. prawda z(x<y or x==y)
nie oznaczax<=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
Uwaga: Python 2 ma dwa rodzaje klas:
-
Styl Klasyczny (lub W starym stylu) klasy, które nie dziedziczą od
object
i które są zadeklarowane jakoclass A:
,class A():
lubclass A(B):
gdzieB
jest klasą w stylu klasycznym; nowy-styl klasy, które dziedziczą z
object
i które są zadeklarowane jakoclass A(object)
lubclass A(B):
, gdzieB
jest klasą nowego stylu. Python 3 ma tylko klasy nowego stylu, które są zadeklarowane jakoclass A:
,class A(object):
lubclass A(B):
.
Dla klasy klasy, operacja porównawcza zawsze wywołuje metodę pierwszego operandu, podczas gdy dla klasy nowego stylu, zawsze wywołuje metodę podklasy operandu, niezależnie od kolejności operandów .
Więc tutaj, jeśli Number
jest stylem klasycznym Klasa:
-
n1 == n3
wywołanian1.__eq__
; -
n3 == n1
wywołanian3.__eq__
; -
n1 != n3
wywołanian1.__ne__
; -
n3 != n1
połączenian3.__ne__
.
I jeśli Number
jest klasą nowego stylu:
- zarówno
n1 == n3
jak in3 == n1
calln3.__eq__
; - zarówno
n1 != n3
jak in3 != n1
zadzwońn3.__ne__
.
Aby rozwiązać problem nieprzemienności operatorów ==
i !=
dla klasy 2 w Pythonie, metody __eq__
i __ne__
powinny zwracać NotImplemented
wartość, gdy typ operandu nie jest obsługiwany. Dokumentacja definiuje wartość NotImplemented
jako:
Metody numeryczne i metody porównawcze rich mogą zwracać tę wartość, jeśli nie realizują operacji dla podanych operandów. (The interpreter spróbuje wtedy operacji odbicia, lub innej w zależności od operatora.) Jego wartość prawdy jest prawdziwa.
W tym przypadku operator przekazuje operację porównania do metoda odbicia zinne operand. Dokumentacja definiuje metody refleksyjne jako:
Nie ma wersji podmienionych argumentów tych metod (do użycia gdy lewy argument nie wspiera operacji, ale prawy argument robi); raczej
__lt__()
i__gt__()
są wzajemnie refleksji,__le__()
i__ge__()
są wzajemnie odbiciem i__eq__()
i__ne__()
są ich własnym odbiciem.
Wynik wygląda następująco to:
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
Zwracanie wartości NotImplemented
zamiast {[61] } jest właściwe nawet dla klas nowego stylu, jeślikomutatywność operatorów ==
i !=
jest pożądana, gdy operandy są niepowiązanych typów (bez dziedziczenia).
Jesteśmy już na miejscu? Niezupełnie. Ile mamy unikalnych numerów?
len(set([n1, n2, n3])) # 3 -- oops
Sets używa skrótów obiektów i domyślnie Python zwraca hash identyfikatora obiektu. Spróbujmy obejść it:
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
len(set([n1, n2, n3])) # 1
Wynik końcowy wygląda tak (dodałem kilka twierdzeń na końcu dla walidacji):
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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-10 21:56:28
Musisz uważać na dziedziczenie:
>>> class Foo:
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
else:
return False
>>> class Bar(Foo):pass
>>> b = Bar()
>>> f = Foo()
>>> f == b
True
>>> b == f
False
Sprawdzaj typy ściślej, jak to:
def __eq__(self, other):
if type(other) is type(self):
return self.__dict__ == other.__dict__
return False
Poza tym, twoje podejście będzie działać dobrze, po to są specjalne metody.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-12-24 02:30:13
Sposób, w jaki opisujesz, to sposób, w jaki zawsze to robiłem. Ponieważ jest całkowicie ogólna, zawsze możesz podzielić tę funkcjonalność na klasę mixin i dziedziczyć ją w klasach, w których chcesz tę funkcjonalność.
class CommonEqualityMixin(object):
def __eq__(self, other):
return (isinstance(other, self.__class__)
and self.__dict__ == other.__dict__)
def __ne__(self, other):
return not self.__eq__(other)
class Foo(CommonEqualityMixin):
def __init__(self, item):
self.item = item
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-10-07 22:01:02
Nie jest to bezpośrednia odpowiedź, ale wydaje się na tyle istotna, że można ją wykorzystać, ponieważ oszczędza to trochę gadatliwej nudy przy okazji. Prosto od lekarzy...
Functools.total_ordering (cls)
Biorąc pod uwagę klasę definiującą jedną lub więcej bogatych metod zamawiania porównań, ta klasa dostarcza resztę. upraszcza to wysiłek związany z określeniem wszystkich możliwych bogatych operacji porównawczych:
Klasa musi zdefiniować jedną z lt (), le(), gt () lub ge(). Ponadto klasa powinna dostarczyć metodę eq ().
Nowość w wersji 2.7
@total_ordering
class Student:
def __eq__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) ==
(other.lastname.lower(), other.firstname.lower()))
def __lt__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) <
(other.lastname.lower(), other.firstname.lower()))
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-20 00:34:26
Nie musisz nadpisywać zarówno __eq__
jak i __ne__
możesz nadpisać tylko __cmp__
ale to spowoduje implikację na wyniku==,!= = , i tak dalej.
is
testy tożsamości obiektu. Oznacza to, że a is
b będzie True
W przypadku, gdy a i b zawierają odniesienie do tego samego obiektu. W Pythonie zawsze przechowujesz odniesienie do obiektu w zmiennej, a nie do rzeczywistego obiektu, więc zasadniczo, Aby a było prawdą, obiekty w nich powinny znajdować się w tej samej pamięci miejsce. W jaki sposób i co najważniejsze, dlaczego miałby Pan to ignorować?
Edit: nie wiedziałem, że __cmp__
został usunięty z Pythona 3, więc go unikaj.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-03 06:14:44
Od tej odpowiedzi: https://stackoverflow.com/a/30676267/541136 udowodniłem, że chociaż poprawne jest definiowanie __ne__
w kategoriach __eq__
- zamiast
def __ne__(self, other):
return not self.__eq__(other)
Powinieneś użyć:
def __ne__(self, other):
return not self == other
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:29
Myślę, że dwa terminy, których szukasz to equality ( = = ) i identity (is). Na przykład:
>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a == b
True <-- a and b have values which are equal
>>> a is b
False <-- a and b are not the same list object
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-12-23 23:12:07
Test 'is' sprawdzi tożsamość przy użyciu wbudowanej funkcji ' id ()', która zasadniczo zwraca adres pamięci obiektu i dlatego nie jest przeciążalna.
Jednak w przypadku testowania równości klasy prawdopodobnie chcesz być trochę bardziej rygorystyczny w swoich testach i porównywać tylko atrybuty danych w swojej klasie:
import types
class ComparesNicely(object):
def __eq__(self, other):
for key, value in self.__dict__.iteritems():
if (isinstance(value, types.FunctionType) or
key.startswith("__")):
continue
if key not in other.__dict__:
return False
if other.__dict__[key] != value:
return False
return True
Ten kod porównuje tylko niefunkcyjne dane członków twojej klasy, a także pomija wszystko prywatne, co jest ogólnie czego chcesz. W przypadku zwykłych starych obiektów Pythona mam klasę bazową, która implementuje _ _ INIT__, _ _ str__,__ repr _ _ i _ _ eq__, więc moje obiekty POPO nie przenoszą ciężaru całej tej dodatkowej (i w większości przypadków identycznej) logiki.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-12-24 03:00:53