2015-04-17 5 views
5

Il Python 2.7 docs stato che assertItemsEqual "è l'equivalente di assertEqual(sorted(expected), sorted(actual))". Nell'esempio seguente, tutti i test passano tranne test4. Perché assertItemsEqual fallisce in questo caso?Perché un assertEqual di successo non implica sempre un assertItemsEqual di successo?

Per il principio di minimo stupore, dati due iterables, mi aspetto che un successo assertEqual implichi un successo assertItemsEqual.

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

Ecco l'output sulla mia macchina:

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

Perché non hai definito un ordinamento su oggetti 'pippo'? – wim

+0

Grazie. Al tuo punto, test4 passa se definisco un metodo __hash__ su 'pippo'. Tuttavia, i documenti affermano che assertItemsEqual funziona su oggetti non selezionabili. Sto fraintendendo il documento? –

+0

Non conosco molto bene Python, ma il messaggio di errore ti dice chiaramente che sta confrontando gli elenchi contando gli oggetti unici in entrambi e confrontando i codici chiave per chiave. I confronti sono per indirizzo dell'oggetto. Dal momento che esistono diverse istanze di oggetti in ciascuna lista, gli elenchi vengono confrontati come non uguali. Se hai detto 'a = foo (1); b = pippo (2); self.list1 = [a, b] self.list2 = [b, a] ', scommetto che l'ultimo test sarebbe passato. – Gene

risposta

3

Le specifiche del documento sono scollegate in modo interessante dall'implementazione, che non esegue mai alcun ordinamento. Here is the source code. Come puoi vedere, prima cerca di contare per hashing usando collections.Counter. Se questo non riesce con un errore di tipo (poiché entrambi gli elenchi contengono un elemento che è inaffidabile), passa a a second algorithm, dove viene confrontato utilizzando i loop python == e O (n^2).

Quindi, se la vostra classe foo non fosse in grado di resistere, il secondo algoritmo segnalerebbe una corrispondenza. Ma è perfettamente lavabile. Dai documenti:

Gli oggetti che sono istanze di classi definite dall'utente sono disponibili per impostazione predefinita; si confrontano tutti ineguali (tranne che con se stessi) e il loro valore hash deriva dal loro id().

Ho verificato questo chiamando collections.Counter([foo(1)]). Nessuna eccezione di errore di tipo.

Quindi qui è dove il codice viene fuori dai binari.Dalla documentazione in __hash__:

se definisce cmp() o eq(), ma non hash(), le sue istanze non saranno utilizzabili in collezioni hash.

Sfortunatamente "non utilizzabile" a quanto pare non equivale a "inaffondabile".

E continua dicendo:

classi che ereditano un metodo hash() da una classe padre, ma cambiare il significato di cmp() o eq() in modo che l'hash il valore restituito non è più appropriato (ad esempio passando a un concetto di uguaglianza basato sul valore anziché l'uguaglianza basata sull'identità predefinita) può dichiararsi esplicitamente inaffidabile impostando hash = Nessuno nella definizione della classe.

Se ridefiniamo:

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 

tutti i test passano!

Quindi sembra che i documenti non siano esattamente sbagliati, ma non sono nemmeno abbastanza chiari. Dovrebbero menzionare che il conteggio è fatto con l'hashing e solo se ciò fallisce è provato il semplice confronto delle uguaglianze. Questo è solo un approccio valido se gli oggetti hanno una semantica di hashing completa o sono completamente inaffidabili. I tuoi erano nella terra di mezzo. (Credo che Python 3 sia più rigoroso nel non consentire o almeno di mettere in guardia contro classi di questo tipo.)

+0

Grazie, Gene. La tua risposta è chiaramente scritta e spiega la causa principale --- Apprezzo in particolare che tu abbia fondato la tua risposta nel codice sorgente di Python. –

+0

Prego. È una domanda ben scritta. La prossima cosa interessante è che ho provato a impostare '__hash__ = None' come raccomandato per le classi utente che impostano' __eq__' ma non '__hash__'. Abbastanza sicuro, il secondo algoritmo funziona perché questo rende 'foo' inaffidabile. Ma a quanto pare ha un bug! Ottengo un'eccezione, il campo 'a' non esiste. C'è un oggetto fasullo che viene confrontato. Ho bisogno di dormire un po '. Divertiti con esso. – Gene

+0

@MatthewNizol Ho capito cosa stava succedendo. Il secondo algoritmo funziona scrivendo i riferimenti a un oggetto NULL rispetto a quelli già confrontati. Il tuo '__eq__' stava fallendo su questi refs perché NULL non aveva campo' a'. L'ho risolto nel codice sopra. Buona notte! – Gene

3

La parte rilevante dei documenti è qui:

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

maggior parte degli altri oggetti di tipi built-in confronta ineguale a meno che non siano lo stesso oggetto; la scelta se un oggetto è considerato più piccolo o più grande di un altro viene fatto arbitrariamente ma in modo coerente all'interno di un'esecuzione di un programma.

Quindi, se fate x, y = foo(1), foo(1), allora non è ben definito se l'ordinamento finisce come x > y o x < y. In python3 non ti sarebbe affatto permesso, la chiamata sorted dovrebbe generare un'eccezione.

Dal momento che le chiamate di Unestest setUp per ogni metodo di prova, si ottengono diverse istanze foo create ogni volta.


assertItemsEqual è implementato con un collections.Counter (una sottoclasse di dict), quindi penso il fallimento di test4 può essere un sintomo di questo fatto:

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

Se due elementi risultano uguali, quindi dovrebbero fare lo stesso hash, altrimenti si rischia di rompere mappature come questa.

+0

Grazie, wim. Vedo il tuo punto di vista sul fatto che la chiamata sort() potrebbe produrre un comportamento indefinito. Tuttavia, ho provato il comportamento dopo aver aggiunto un metodo __cmp__ a foo (restituisce -1 se self.a other.a), e test4 continua a fallire. Test4 riesce se aggiungo un metodo __hash__ (restituendo self.a); quindi sembra che assertItemEqual stia raggruppando in base al valore hash.Tuttavia, i documenti non sembrano indicare questo requisito. –

+0

Penso che sia perché 'assertItemsEqual' è implementato con un' collections.Counter'. Interessante. Lasciatemi indagare ulteriormente .. – wim

+0

Grazie mille per la ricerca. Sei giunto alla stessa conclusione che Gene ha fatto --- che la causa principale è l'uso di 'collections.Counter' nell'implementazione. Tuttavia, dato che posso accettare solo una risposta, ho accettato Gene a causa della sua osservazione aggiuntiva che tutti gli oggetti definiti dall'utente sono disponibili per default tramite il loro id(), che chiarisce perché 'collections.Counter' raggruppa le istanze foo come fa. Grazie ancora! –