Python sınıflarında denkliği (“eşitlik”) desteklemenin zarif yolları


421

Özel sınıflar yazarken ==ve !=operatörleri aracılığıyla denkliğe izin vermek genellikle önemlidir . Python'da bu , sırasıyla __eq__ve __ne__özel yöntemlerin uygulanmasıyla mümkün olur . Bunu yapmanın en kolay yolu aşağıdaki yöntemdir:

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)

Bunu yapmanın daha zarif yollarını biliyor musunuz? Yukarıdaki karşılaştırma yöntemini kullanmanın herhangi bir dezavantajı biliyor musunuz __dict__?

Not : Biraz açıklama - ne zaman __eq__ve ne zaman __ne__tanımsızsanız, bu davranışı bulacaksınız:

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False

Yani, gerçekten çalıştığı a == biçin değerlendirir , bir kimlik testi (yani, " Aynı nesne mi?").Falsea is bab

Ne zaman __eq__ve __ne__tanımlandığından, (bir Peşinde olduğumuz olan) bu davranışı bulabilirsiniz:

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True

6
+1, çünkü diktenin == için üyeliğe eşitlik kullandığını bilmiyordum, bunun sadece aynı nesne dikimleri için eşit sayıldığını varsaymıştım. Sanırım bu açıktır çünkü Python isnesne kimliğini değer karşılaştırmasından ayırmak için operatöre sahiptir .
SingleNegationElimination

5
Kabul edilen cevabın düzeltildiğini veya Algorias'ın cevabına yeniden atandığını, böylece katı tip kontrolünün uygulanacağını düşünüyorum.
en fazla

1
Ayrıca karmanın geçersiz kılındığından emin olun stackoverflow.com/questions/1608842/…
Alex Punnen

Yanıtlar:


328

Bu basit sorunu düşünün:

class Number:

    def __init__(self, number):
        self.number = number


n1 = Number(1)
n2 = Number(1)

n1 == n2 # False -- oops

Bu nedenle, Python varsayılan olarak karşılaştırma işlemleri için nesne tanımlayıcılarını kullanır:

id(n1) # 140400634555856
id(n2) # 140400634555920

Fonksiyonu geçersiz __eq__kılmak sorunu çözüyor gibi görünüyor:

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

In Python 2 , her zaman geçersiz kılmak için hatırlamak __ne__gibi, hem de işlevi belgeleri devletler:

Karşılaştırma işleçleri arasında herhangi bir zımni ilişki yoktur. Gerçek, bunun yanlış olduğu x==yanlamına gelmez x!=y. Buna göre, tanımlarken __eq__(), __ne__()operatörlerin beklendiği gibi davranması için de tanımlanmalıdır .

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    return not self.__eq__(other)


n1 == n2 # True
n1 != n2 # False

In Python 3 , bu gibi artık gerekli değildir belgeleri devletler:

Varsayılan olarak, sonuca __ne__()yetki __eq__()verilmez ve sonuç ters çevrilir NotImplemented. Karşılaştırma operatörleri arasında başka hiçbir zımni ilişki yoktur, örneğin, gerçeği (x<y or x==y)ima etmez x<=y.

Ancak bu tüm sorunlarımızı çözmez. Bir alt sınıf ekleyelim:

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

Not: Python 2'de iki tür sınıf vardır:

  • Klasik tarzdaki (veya eski tarz do) sınıfları, değil devralmakobjectve ilan edilirclass A:,class A():ya daclass A(B):neredeBbir klasik tarzda sınıftır;

  • Yeni tarzı devralan do sınıfları,objectolarak, beyanclass A(object)veyaclass A(B):neredeByeni tarzı sınıftır. Python 3 olarak ilan edilmiştir yalnızca yeni tarzı sınıfları vardırclass A:,class A(object):ya daclass A(B):.

Klasik stil sınıfları için, karşılaştırma işlemi her zaman ilk işlenenin yöntemini çağırırken, yeni stil sınıfları için, işlenenlerin sırasından bağımsız olarak her zaman alt sınıf işleneninin yöntemini çağırır .

Burada Numberklasik tarzda bir sınıf varsa:

  • n1 == n3çağrılar n1.__eq__;
  • n3 == n1çağrılar n3.__eq__;
  • n1 != n3çağrılar n1.__ne__;
  • n3 != n1çağrıları n3.__ne__.

Ve eğer Numberyeni bir sınıf ise:

  • her ikisi de n1 == n3ve n3 == n1çağrı n3.__eq__;
  • hem n1 != n3ve n3 != n1çağrı n3.__ne__.

Python 2 klasik tarzı sınıfların ==ve !=işleçlerinin değişmezlik sorununu gidermek için , işlenen türü desteklenmediğinde __eq__ve __ne__yöntemlerinin NotImplementeddeğeri döndürmesi gerekir . Belgeler tanımlar NotImplementeddeğeri olarak:

Sayısal yöntemler ve zengin karşılaştırma yöntemleri, sağlanan işlenenler için işlemi uygulamazlarsa bu değeri döndürebilir. (Daha sonra yorumlayıcı, operatöre bağlı olarak yansıtılan işlemi veya başka bir geri dönüşü dener.) Gerçek değeri doğrudur.

Bu durumda, operatör delegelerinin karşılaştırma işlemi yöntemi yansıyan ait diğer işlenen. Belgeler tanımlar yöntemleri olarak yansıtılır:

Bu yöntemlerin takas argümanı sürümleri yoktur (sol argüman işlemi desteklemiyor, ancak sağ argüman destekliyorsa kullanılır); daha doğrusu, __lt__()ve __gt__()birbirlerinin yansımasıdır, __le__()ve __ge__()birbirlerinin yansımasıdır ve __eq__()ve __ne__()kendi yansımasıdır.

Sonuç şuna benzer:

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

Geri dönen NotImplementedyerine değerini Falseeğer yeni tarzı sınıflar için bile yapılacak doğru şey olduğunu Yerdeğiştirme ait ==ve !=işlenenler ilgisiz türleri (hiçbir miras) ait olduğunda operatörleri arzu edilir.

Henüz varmadık mı? Pek değil. Kaç tane benzersiz numaramız var?

len(set([n1, n2, n3])) # 3 -- oops

Kümeler nesnelerin karmasını kullanır ve varsayılan olarak Python nesne tanımlayıcısının karmasını döndürür. Geçersiz kılmaya çalışalım:

def __hash__(self):
    """Overrides the default implementation"""
    return hash(tuple(sorted(self.__dict__.items())))

len(set([n1, n2, n3])) # 1

Sonuç şu şekilde görünüyor (Doğrulama için bazı iddialar ekledim):

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

3
hash(tuple(sorted(self.__dict__.items())))değerleri arasında herhangi bir hash edilemeyen nesne self.__dict__varsa (örneğin, nesnenin niteliklerinden herhangi biri, örneğin a list) olarak ayarlanmışsa çalışmaz .
en fazla

3
Doğru, ama sonra varsınızda () böyle mutable nesneler varsa iki nesne gerçekten eşit değildir ...
Tal Weiss


1
Üç açıklama: 1. Python 3'te, __ne__artık uygulamaya gerek yok: "Varsayılan olarak, __ne__()delegeler__eq__() sonuca verilmez ve sonuç tersine çevrilir NotImplemented". Biri hala uygulamak isterse 2. __ne__, daha genel bir uygulama (Python 3 bence tarafından kullanılan bir) geçerli: x = self.__eq__(other); if x is NotImplemented: return x; else: return not x. 3. Verilen __eq__ve __ne__uygulamalar yetersizdir: if isinstance(other, type(self)):22 __eq__ve 10 __ne__çağrı verirken if isinstance(self, type(other)):, 16 __eq__ve 6 __ne__çağrı verir.
Maggyero

4
Zarafeti sordu, ama sağlamlaştı.
GregNash

201

Kalıtım konusunda dikkatli olmalısınız:

>>> 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

Tipleri daha sıkı kontrol edin, şöyle:

def __eq__(self, other):
    if type(other) is type(self):
        return self.__dict__ == other.__dict__
    return False

Bunun yanı sıra, yaklaşımınız iyi çalışacaktır, özel yöntemler budur.


Bu iyi bir nokta. Sanırım, türlerde inşa edilen alt sınıflamanın her iki yönde de eşitliğe izin verdiğini ve aynı tipte olup olmadığını kontrol etmek bile istenmeyen olabilir.
gotgenes

12
Türleri karşılaştırması için temsilci farklı, NotImplemented dönmek öneririz.
max

4
@maks karşılaştırması sol tarafın (LHS) sağ tarafa (RHS), ardından RHS ila LHS'nin yapılması gerekmez; bkz. stackoverflow.com/a/12984987/38140 . Yine de, NotImplementedönerdiğiniz gibi geri dönmek her zaman neden olur superclass.__eq__(subclass), bu da istenen davranıştır.
gotgenes

4
Bir ton üyeniz varsa ve etrafta oturan çok sayıda nesne kopyası yoksa, bir başlangıç ​​kimlik testi eklemek genellikle iyidir if other is self. Bu, daha uzun sözlük karşılaştırmasını önler ve nesneler sözlük anahtarı olarak kullanıldığında büyük tasarruflar sağlayabilir.
Dane White

2
Ve uygulamayı unutmayın__hash__()
Dane White

161

Tarif ettiğiniz şekilde her zaman yaptığım yöntemdir. Tamamen genel olduğundan, bu işlevselliği her zaman bir mixin sınıfına ayırabilir ve bu işlevselliği istediğiniz sınıflarda devralabilirsiniz.

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

6
+1: Alt sınıflarda kolay değişim için strateji modeli.
S.Lott

3
isinstance berbat. Neden kontrol etmeliyim? Neden sadece self .__ dict__ == diğer .__ dict__?
nosklo

3
@nosklo: Anlamıyorum .. tamamen ilgisiz sınıflardan iki nesne aynı özelliklere sahip olursa?
en fazla

1
Nokslo'nun isinstance'ı atlamayı önerdiğini düşündüm. Bu durumda artık otherbir alt sınıfının olup olmadığını bilemezsiniz self.__class__.
maksimum

10
__dict__Karşılaştırma ile ilgili başka bir sorun , eşitlik tanımınızda dikkate almak istemediğiniz bir özelliğiniz varsa (örneğin, benzersiz bir nesne kimliği veya zaman oluşturulmuş bir damga gibi meta veriler).
Adam Parkin

14

Doğrudan bir cevap değil, ama zaman zaman biraz ayrıntılı kurtarır kaydetti gibi tacked için uygun görünüyordu. Doğrudan dokümanlardan kesin ...


functools.total_ordering (CLS)

Bir veya daha fazla zengin karşılaştırma siparişi yöntemini tanımlayan bir sınıf verildiğinde, bu sınıf dekoratörü geri kalanını sağlar.Bu, olası zengin karşılaştırma işlemlerinin tümünü belirleme çabalarını basitleştirir:

Sınıf biri tanımlamalıdır __lt__(), __le__(), __gt__()ya da__ge__() . Ayrıca, sınıf bir __eq__()yöntem sağlamalıdır .

2.7 sürümündeki yenilikler

@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()))

1
Ancak total_ordering'in ince tuzakları vardır: regebro.wordpress.com/2010/12/13/… . Farkında olmak !
Mr_and_Mrs_D

8

Her ikisini de geçersiz kılmak zorunda değilsiniz __eq__ve __ne__yalnızca geçersiz kılabilirsiniz__cmp__ ancak bu ==,! ==, <,> vb. Sonucuna etki eder.

isnesne kimliği testleri. Bu, bir isb'ninTrue , a ve b'nin her ikisinin de aynı nesneye referansı tutması durumunda . Python'da her zaman asıl nesne değil, bir değişken içindeki bir nesneye başvuruda bulunursunuz, bu nedenle esas olarak a b'nin doğru olması için içindeki nesnelerin aynı bellek konumunda bulunması gerekir. Nasıl ve en önemlisi bu davranışı geçersiz kılmayı tercih edersiniz?

Düzenleme: __cmp__Python 3 kaldırıldı bilmiyordum bu yüzden kaçının.


Çünkü bazen nesneleriniz için farklı bir eşitlik tanımınız olur.
Ed S.

is operatörü, tercümanlara nesne kimliğine cevap verir, ancak yine de cmp'yi
Vasil

7
Python 3'te, "cmp () işlevi gitti ve __cmp __ () özel yöntemi artık desteklenmiyor." is.gd/aeGv
gotgenes


2

Aradığınız iki terimin eşitlik (==) ve kimlik (is) olduğunu düşünüyorum. Örneğin:

>>> 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

1
Belki de, sadece iki listedeki ilk iki öğeyi karşılaştıran bir sınıf oluşturabilir ve bu öğeler eşitse True olarak değerlendirilir. Bu eşitlik, bence eşitlik değil. Denklem için mükemmel derecede geçerli .
gotgenes

Ancak, kabul ediyorum ki "is" bir kimlik sınavıdır.
gotgenes

1

'İs' testi, temelde nesnenin bellek adresini döndüren ve dolayısıyla yüklenemeyen yerleşik 'id ()' işlevini kullanarak kimliği test eder.

Bununla birlikte, bir sınıfın eşitliğini test etmeniz durumunda, muhtemelen testleriniz hakkında biraz daha katı olmak ve sadece sınıfınızdaki veri özelliklerini karşılaştırmak istersiniz:

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

Bu kod, yalnızca sınıfınızın işlev dışı veri üyelerini karşılaştıracak ve genellikle istediğiniz şey olan özel bir şeyi atlayacaktır. Düz Eski Python Nesneleri durumunda, __init__, __str__, __repr__ ve __eq__'yi uygulayan bir temel sınıfım var, böylece POPO nesnelerim tüm bu ekstra (ve çoğu durumda aynı) mantığın yükünü taşımıyor.


Bit nitpicky, ancak id () kullanarak 'is' testleri yalnızca kendi is_ () üye işlevinizi (2.3+) tanımlamadıysanız test eder. [ docs.python.org/library/operator.html]
harcandı

Ben "geçersiz kılma" ile aslında maymun modül yama yama demek istiyorum. Bu durumda ifadeniz tamamen doğru değildir. Operatör modülü kolaylık sağlamak için sağlanmıştır ve bu yöntemlerin geçersiz kılınması "is" operatörünün davranışını etkilemez. "İs" kullanan bir karşılaştırma, karşılaştırma için her zaman bir nesnenin kimliğini () kullanır, bu davranış geçersiz kılınamaz. Ayrıca is_ üye işlevinin karşılaştırma üzerinde bir etkisi yoktur.
mcrute

mcrute - Çok erken (ve yanlış) konuştum, kesinlikle haklısın.
harcandı

Bu çok güzel bir çözüm, özellikle __eq__de ilan edildiğinde CommonEqualityMixin(diğer cevaba bakınız). Bunu özellikle SQLAlchemy'de Base'den türetilen sınıfların örneklerini karşılaştırırken yararlı buldum. Karşılaştırılamaz için _sa_instance_stateDeğiştim key.startswith("__")):için key.startswith("_")):. Onlarda bazı referanslar da vardı ve Algorias'ın cevabı sonsuz özyineleme yarattı. Böylece, '_'karşılaştırma sırasında da atlanmaları için tüm geri başvuruları adlandırdım . NOT: Python 3.x değişimde iteritems()için items().
Wookie88

@mcrute Genellikle, __dict__bir örneğin __kullanıcı tarafından tanımlanmadığı sürece başlayan hiçbir şey yoktur . Yapılacaklar gibi __class__, __init__vb örnek yıllarda değildir __dict__, daha ziyade kendi sınıfında __dict__. OTOH, özel niteliklerle kolayca başlayabilir __ve muhtemelen kullanılmalıdır __eq__. __Önceden düzeltilmiş öznitelikleri atlarken tam olarak neyi önlemeye çalıştığınızı açıklayabilir misiniz ?
maksimum

1

Alt sınıf / mixins kullanmak yerine, genel bir sınıf dekoratör kullanmak istiyorum

def comparable(cls):
    """ Class decorator providing generic comparison functionality """

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__

    def __ne__(self, other):
        return not self.__eq__(other)

    cls.__eq__ = __eq__
    cls.__ne__ = __ne__
    return cls

Kullanımı:

@comparable
class Number(object):
    def __init__(self, x):
        self.x = x

a = Number(1)
b = Number(1)
assert a == b

0

Bu, Algorias'ın cevabı hakkındaki yorumları içerir ve nesneleri tek bir özellikle karşılaştırır, çünkü tüm dikdörtgeni umursamıyorum. hasattr(other, "id")doğru olmalı, ama yapıcıya koyduğum için biliyorum.

def __eq__(self, other):
    if other is self:
        return True

    if type(other) is not type(self):
        # delegate to superclass
        return NotImplemented

    return other.id == self.id
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.