2015-04-17 7 views
5

Der Python 2.7 docs Zustand, dass assertItemsEqual "entspricht assertEqual(sorted(expected), sorted(actual))". Im folgenden Beispiel sind alle Tests mit Ausnahme von test4 bestanden. Warum schlägt assertItemsEqual in diesem Fall fehl?Warum bedeutet ein erfolgreiches assertEqual nicht immer ein erfolgreiches assertItemsEqual?

Nach dem Prinzip der geringsten Verwunderung, gegeben zwei Iterables, würde ich erwarten, dass eine erfolgreiche assertEqual eine erfolgreiche assertItemsEqual impliziert. Hier

import unittest 

class foo(object): 
    def __init__(self, a): 
     self.a = a 

    def __eq__(self, other): 
     return self.a == other.a 

class test(unittest.TestCase): 
    def setUp(self): 
     self.list1 = [foo(1), foo(2)] 
     self.list2 = [foo(1), foo(2)] 

    def test1(self): 
     self.assertTrue(self.list1 == self.list2) 

    def test2(self): 
     self.assertEqual(self.list1, self.list2) 

    def test3(self): 
     self.assertEqual(sorted(self.list1), sorted(self.list2)) 

    def test4(self): 
     self.assertItemsEqual(self.list1, self.list2) 

if __name__=='__main__': 
    unittest.main() 

ist die Ausgabe auf meinem Rechner:

FAIL: test4 (__main__.test) 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
    File "assert_test.py", line 25, in test4 
    self.assertItemsEqual(self.list1, self.list2) 
AssertionError: Element counts were not equal: 
First has 1, Second has 0: <__main__.foo object at 0x7f67b3ce2590> 
First has 1, Second has 0: <__main__.foo object at 0x7f67b3ce25d0> 
First has 0, Second has 1: <__main__.foo object at 0x7f67b3ce2610> 
First has 0, Second has 1: <__main__.foo object at 0x7f67b3ce2650> 

---------------------------------------------------------------------- 
Ran 4 tests in 0.001s 

FAILED (failures=1) 
+2

Weil Sie keine Reihenfolge für 'foo' Objekte definiert haben? – wim

+0

Danke. Zu deinem Punkt übergibt test4, wenn ich eine __hash__ Methode auf 'foo' definiere. Die Dokumente geben jedoch an, dass assertItemsEqual für nicht hashbare Objekte funktioniert. Missverstehe ich den Doktor? –

+0

Ich kenne Python nicht sehr gut, aber die Fehlermeldung sagt Ihnen klar, dass es die Listen vergleicht, indem Sie eindeutige Objekte in beiden zählen und dann die Zählungen Schlüssel für Schlüssel vergleichen. Die Vergleiche sind nach Objektadresse. Da Sie in jeder Liste verschiedene Objektinstanzen haben, vergleichen sich die Listen als ungleich. Wenn du gesagt hast: a = foo (1); b = foo (2); self.list1 = [a, b] self.list2 = [b, a] ', ich wette, der letzte Test würde bestehen. – Gene

Antwort

3

Die Dokumentenspezifikation ist interessanterweise von der Implementierung getrennt, die niemals sortiert. Here is the source code. Wie Sie sehen können, versucht es zuerst durch Hashing mit collections.Counter zu zählen. Wenn dies mit einem Typfehler fehlschlägt (weil jede Liste ein Element enthält, das nicht abspeicherbar ist), geht es weiter zu a second algorithm, wo es mit Python == und O (n^2) Schleifen vergleicht.

Wenn also Ihre Klasse foo nicht abspeicherbar ist, würde der zweite Algorithmus eine Übereinstimmung signalisieren. Aber es ist perfekt waschbar. Aus der Dokumentation:

Objekte, die Instanzen von benutzerdefinierten Klassen sind, sind standardmäßig hashbar; sie vergleichen alle ungleich (außer mit sich selbst), und ihr Hash-Wert wird von ihrer ID() abgeleitet.

Ich verifiziert dies durch den Aufruf collections.Counter([foo(1)]). Keine Typfehler-Ausnahme.

So hier ist, wo Ihr Code von den Schienen kommt.Aus der Dokumentation auf __hash__:

wenn sie definiert cmp() oder eq(), aber nicht Hash(), werden seine Instanzen nicht in einem Hash-Sammlungen verwendbar sein.

Leider ist "nicht verwendbar" scheinbar nicht mit "nicht absperrbar" gleichzusetzen.

Es geht weiter zu sagen:

Klassen, die einen Hash() -Methode von einer übergeordneten Klasse erben, aber die Bedeutung von cmp() oder eq() so ändern, dass der Hash Der zurückgegebene Wert ist nicht länger geeignet (z. B. durch Wechsel zu einem wertebasierten Gleichheitskonzept anstelle der standardmäßigen identitätsbasierten Gleichheit) kann sich explizit als nicht abhäsiv kennzeichnen, indem hash = None in der Klassendefinition festgelegt wird.

Wenn wir neu definieren:

class foo(object): 
    __hash__ = None 
    def __init__(self, a): 
     self.a = a 
    def __eq__(self, other): 
     return isinstance(other, foo) and self.a == other.a 

alle Tests bestanden!

So scheint es, die Dokumente sind nicht genau falsch, aber sie sind auch nicht überdeutlich klar. Sie sollten erwähnen, dass das Zählen mit Hashing durchgeführt wird, und nur wenn dies fehlschlägt, wird eine einfache Gleichheitsübereinstimmung versucht. Dies ist nur dann ein gültiger Ansatz, wenn die Objekte entweder eine vollständige Hash-Semantik haben oder vollständig nicht hashbar sind. Dein war in der Mitte. (Ich glaube, dass Python 3 strenger ist, Klassen dieses Typs zu verbieten oder wenigstens davor zu warnen.)

+0

Danke, Gene. Ihre Antwort ist klar geschrieben und erklärt die Ursache - ich weiß es besonders zu schätzen, dass Sie Ihre Antwort im Python-Quellcode begründet haben. –

+0

Gern geschehen. Es ist eine schön geschriebene Frage. Die nächste interessante Sache ist, dass ich versucht habe, '__hash__ = None' zu ​​setzen, wie für Benutzerklassen empfohlen, die' __eq__', aber nicht '__hash__' setzen. Sicher genug, der zweite Algorithmus läuft, weil dadurch 'foo' nicht absperrbar wird. Aber anscheinend hat es einen Fehler! Ich bekomme eine Ausnahme, Feld 'a' existiert nicht. Es gibt ein falsches Objekt, das verglichen wird. Ich muss etwas schlafen. Viel Spass damit. – Gene

+0

@MatthewNizol Ich habe herausgefunden, was los ist. Der zweite Algorithmus schreibt Referenzen auf ein NULL-Objekt über bereits verglichene. Ihr '__eq__' war bei diesen Referenzen nicht erfolgreich, weil NULL kein 'a'-Feld hatte. Ich habe es im obigen Code behoben. Gute Nacht! – Gene

3

Der relevante Teil der Dokumentation ist hier:

https://docs.python.org/2/reference/expressions.html?highlight=ordering#not-in

Die meisten anderen Objekte von eingebauten Typen ungleich vergleichen, es sei denn, sie sind das gleiche Objekt; Die Wahl, ob ein Objekt als kleiner oder größer als ein anderes betrachtet wird, wird willkürlich, aber konsistent innerhalb einer Ausführung eines Programms getroffen.

Also, wenn Sie x, y = foo(1), foo(1) machen, dann ist es nicht gut definiert, ob die Bestellung auf, als x > y oder x < y endet. In python3 wären Sie überhaupt nicht erlaubt, der sorted Aufruf sollte eine Ausnahme auslösen.

Da unittest für jede Testmethode setUp aufruft, erhalten Sie jedes Mal verschiedene foo Instanzen.


assertItemsEqual mit einer collections.Counter (einer Unterklasse von dict) implementiert ist, so dass ich denke, das Scheitern der test4 ein Symptom dieser Tatsache sein kann:

>>> x, y = foo(1), foo(1) 
>>> x == y 
True 
>>> {x: None} == {y: None} 
False 

Wenn zwei Elemente gleich vergleichen, dann sollten sie dasselbe hasen, sonst riskierst du, Mappings wie dieses zu brechen.

+0

Danke, wim. Ich sehe Ihren Punkt, dass der Aufruf sorted() zu undefiniertem Verhalten führen kann. Ich testete das Verhalten jedoch nach dem Hinzufügen einer __cmp__-Methode zu foo (gibt -1 zurück, wenn self.a other.a) und test4 schlägt immer noch fehl. Test4 ist erfolgreich, wenn ich eine __hash__ Methode (return self.a) hinzufüge; Es scheint also, dass assertItemEqual eine Gruppierung basierend auf dem Hash-Wert ist.Die Dokumente scheinen diese Anforderung jedoch nicht anzugeben. –

+0

Ich denke das liegt daran, 'assertItemsEqual' wird mit einem' collections.Counter' implementiert. Interessant. Lassen Sie mich weiter untersuchen .. – wim

+0

Vielen Dank für die Forschung. Sie kamen zu der gleichen Schlussfolgerung, die Gene tat - dass die Ursache die Verwendung von 'collections.Counter' in der Implementierung ist. Da ich jedoch nur eine Antwort akzeptieren kann, akzeptierte ich Gene aufgrund seiner zusätzlichen Beobachtung, dass alle benutzerdefinierten Objekte standardmäßig über ihre ID() hashbar sind, was verdeutlicht, warum 'collections.Counter' die foo-Instanzen so gruppiert. Danke nochmal! –

Verwandte Themen