2013-01-09 11 views
18

Używam modułu Pythona unittest i chcę sprawdzić, czy dwie złożone struktury danych są równe. Obiekty mogą być listami dyktów z różnymi rodzajami wartości: liczbami, łańcuchami znaków, kontenerami w języku Python (listy/krotki/znaki) i tablicami numpy. Te ostatnie są powodem zadać pytanie, bo nie można po prostu zrobićPorównać (potwierdzić równość) dwie złożone struktury danych zawierające numpy tablice w unittest

self.assertEqual(big_struct1, big_struct2) 

ponieważ wytwarza

ValueError: The truth value of an array with more than one element is ambiguous. 
Use a.any() or a.all() 

Wyobrażam sobie, że muszę napisać własny testu równości dla tego produktu. Powinno działać dla dowolnych struktur. Mój obecny pomysł jest rekurencyjna funkcja:

  • próbuje bezpośrednie porównanie obecnego „węzła” w arg1 do analogicznego węzła arg2;
  • jeśli nie ma wyjątków, porusza się (tutaj przetwarzane są także "końcowe" węzły/liście);
  • jeśli zostanie przechwycony ValueError, będzie głębiej, dopóki nie znajdzie numpy.array;
  • porównuje tablice (np. like this).

To, co wydaje się trochę problematyczne, to śledzenie "odpowiadających" węzłów dwóch struktur, ale być może potrzebne jest tutaj tylko zip.

Pytanie brzmi: czy istnieją dobre (prostsze) alternatywy dla tego podejścia? Może numpy przedstawia kilka narzędzi do tego? Jeśli nie zostaną zaproponowane żadne alternatywy, zaimplementuję ten pomysł (chyba że mam lepszy) i opublikuję jako odpowiedź.

P.S. Mam niejasne przekonanie, że mogłem zobaczyć pytanie dotyczące tego problemu, ale nie mogę go teraz znaleźć.

P.P.S. Alternatywnym podejściem byłoby funkcja przechodząca przez strukturę i przekształcająca wszystkie listy, ale czy jest to łatwiejsze do wdrożenia? Wydaje mi się to samo.


Edit: Utworzenie podklasy numpy.ndarray brzmi bardzo obiecująco, ale oczywiście nie mam obie strony stosunku zakodowane w teście. Jednak jeden z nich jest rzeczywiście zakodowany, więc mogę:

  • zapełnić go niestandardowymi podklasami numpy.array;
  • zmiana isinstance(other, SaneEqualityArray) na isinstance(other, np.ndarray) w jterrace's answer;
  • zawsze używaj go jako LHS w porównaniach.

Moje pytania w tym zakresie są:

  1. będzie działać (mam na myśli, to brzmi dobrze dla mnie, ale może jakieś skomplikowane przypadki brzegowe nie będą obsługiwane poprawnie)? Czy mój niestandardowy obiekt zawsze kończy się jako LHS w rekurencyjnych sprawdzaniach równości, jak się spodziewam?
  2. Ponownie, czy istnieją lepsze sposoby (biorąc pod uwagę, że otrzymuję co najmniej jedną strukturę z prawdziwymi tablicami numpy).

Edycja 2: próbowałem go, realizacja (pozornie) pracy jest pokazany na this answer.

+0

Wyobrażam sobie, że pisanie testu równości, który działa na dowolnych struktur danych byłoby dowolnie trudne. Czy naprawdę nie ma dla nich stałej struktury? – goncalopp

+0

@ Gonkalopp Jest ich kilka, dość zawiłych i teoretycznie podlegają zmianom. Nie chcę na tym polegać, zwłaszcza, że ​​nie znam sposobu porównywania wszystkiego z wyjątkiem 'X'_ w dwóch strukturach, nawet jeśli wiem, gdzie jest' X'. –

+0

Następnie, osobiście, zastosowałem podejście funkcji rekursywnej. Najpierw jednak dokładnie sprawdziłbym typ obiektu - wykonanie ślepego porównania jako pierwszego kroku może być dobre, ale byłoby marnotrawstwem, gdyby struktury danych były duże, ponieważ wartości będą musiały zostać ponownie sprawdzone, jeśli: ValueError' jest podniesiony. – goncalopp

Odpowiedz

7

Więc pomysł ilustruje jterrace wydaje się działać dla mnie z niewielkimi zmianami:

class SaneEqualityArray(np.ndarray): 
    def __eq__(self, other): 
     return (isinstance(other, np.ndarray) and self.shape == other.shape and 
      np.allclose(self, other)) 

Tak jak mówiłem, pojemnik z tych obiektów powinna znajdować się po lewej stronie czeku równości. Tworzę SaneEqualityArray obiektów z istniejących numpy.ndarray s tak:

SaneEqualityArray(my_array.shape, my_array.dtype, my_array) 

zgodnie z podpisem ndarray Konstruktor:

ndarray(shape, dtype=float, buffer=None, offset=0, 
     strides=None, order=None) 

Ta klasa jest zdefiniowana w zestaw testów i służy wyłącznie do celów testowych. RHS sprawdzania równości jest rzeczywistym obiektem zwróconym przez testowaną funkcję i zawiera prawdziwe obiekty: numpy.ndarray.

P.S. Podziękowania dla autorów obu opublikowanych do tej pory odpowiedzi były bardzo pomocne. Jeśli ktoś zauważy jakiekolwiek problemy z tym podejściem, będę wdzięczny za Twoją opinię.

8

Funkcja assertEqual wywoła metodę obiektów, która powinna być rekurencyjna dla złożonych typów danych. Wyjątkiem jest numpy, który nie ma rozsądnej metody. Korzystanie z numpy subclass from this question można przywrócić zdrowie psychiczne do zachowania równości:

import copy 
import numpy 
import unittest 

class SaneEqualityArray(numpy.ndarray): 
    def __eq__(self, other): 
     return (isinstance(other, SaneEqualityArray) and 
       self.shape == other.shape and 
       numpy.ndarray.__eq__(self, other).all()) 

class TestAsserts(unittest.TestCase): 

    def testAssert(self): 
     tests = [ 
      [1, 2], 
      {'foo': 2}, 
      [2, 'foo', {'d': 4}], 
      SaneEqualityArray([1, 2]), 
      {'foo': {'hey': SaneEqualityArray([2, 3])}}, 
      [{'foo': SaneEqualityArray([3, 4]), 'd': {'doo': 3}}, 
      SaneEqualityArray([5, 6]), 34] 
     ] 
     for t in tests: 
      self.assertEqual(t, copy.deepcopy(t)) 

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

Ten test przechodzi.

+0

Zamiast mieszać z '__eq__', mogłem * dużo * raczej zepsuć z' __nonzero__'. chociaż istnieją powody, dla których numpy robi to tak, jak robi to, po prostu zaprasza błędów. – seberg

+0

Dziękuję bardzo za link (teraz wygląda na to, że widziałem go wcześniej)! Ale testuję funkcję, która zwraca rzeczy za pomocą 'numpy.array', a nie jego niestandardowych podklas. Jak wykonać konwersję przed stwierdzeniem? (lub w inny sposób muszę usunąć 'isinstance (other, SaneEqualityArray)' z '__eq__', czy to dobry pomysł?) –

+0

Również @seberg, czy możesz wyjaśnić swoje obawy dotyczące przesłonięcia' __eq__'? (i może przeliterować sugestię dotyczącą '__nonzero__' dla mnie: biorę to musi być wywołany na' AB'? Więc 'AB' powinien dać instancję podklasy? Czy to nie znaczy, że również muszę przesłonić' __sub__ '?) –

12

Byłby skomentował, ale robi się zbyt długo ...

Fun Fakt, nie można używać == aby sprawdzić, czy są takie same tablice Proponuję użyć np.testing.assert_array_equal zamiast.

  1. że sprawdza dtype, kształt itd
  2. że nie uda za miejste matematyki z (float('nan') == float('nan')) == False (normalna sekwencja pyton == ma jeszcze bardziej przyjemny sposób ignorowania tego czasami, bo wykorzystuje PyObject_RichCompareBool który robi (dla Koncepcja nieliczby niepoprawne) is szybkie sprawdzenie (do testowania oczywiście, że jest doskonały) ...
  3. Istnieje również assert_allclose ponieważ równość pływający punkt może być bardzo trudne, jeśli nie rzeczywistych obliczeń i zazwyczaj chcą prawie the te same wartości, ponieważ wartości mogą zależeć od sprzętu lub być losowe w zależności od tego, co z nimi zrobisz.

Prawie sugerowałbym próbowanie serializowania go z marynatą, jeśli chcesz coś tak niesamowicie zagnieżdżonego, ale to jest zbyt surowe (i punkt 3 jest oczywiście w pełni zepsuty), na przykład układ pamięci twojej tablicy nie ale ma znaczenie dla jego serializacji.

+0

Serializing with 'pickle' [ma swoje własne problemy] (http://bugs.python.org/issue6784) ... Dobrze wiedzieć o narzędziach' numpy.testing', ale nadal nie jestem pewien jak się zastosować oni tutaj. –

2

Chciałbym zdefiniować własną metodę assertNumpyArraysEqual(), która jawnie dokonuje porównania, którego chcesz użyć. W ten sposób Twój kod produkcyjny pozostanie niezmieniony, ale możesz nadal dokonywać uzasadnionych twierdzeń w testach jednostkowych. Upewnij się go zdefiniować w module, który zawiera __unittest = True tak, że nie zostaną uwzględnione w ślady stosu:

import numpy 
__unittest = True 

def assertNumpyArraysEqual(self, other): 
    if self.shape != other.shape: 
     raise AssertionError("Shapes don't match") 
    if not numpy.allclose(self, other) 
     raise AssertionError("Elements don't match!") 
+0

Dzięki, to jest dobry pomysł i byłby najlepszą opcją w przypadku, gdy obie tablice są generowane przez testowane funkcje. –

0

budynek na @dbw (z podziękowaniami), następujący sposób włożony w test-case podklasy działa dobrze dla mnie:

def assertNumpyArraysEqual(self,this,that,msg=''): 
    ''' 
    modified from http://stackoverflow.com/a/15399475/5459638 
    ''' 
    if this.shape != that.shape: 
     raise AssertionError("Shapes don't match") 
    if not np.allclose(this,that): 
     raise AssertionError("Elements don't match!") 

miałem to nazywane jako self.assertNumpyArraysEqual(this,that) wewnątrz moich metod przypadku testowego i pracował jak czar.

1

Zabrakło mi do tego samego problemu i opracowali funkcję porównywania równości w oparciu o stworzenie stałego mieszania dla obiektu. Ma to tę dodatkową zaletę, że można przetestować, czy obiekt jest zgodny z oczekiwaniami, porównując jego wartość mieszającą z ustaloną wartością w kodzie.

Kod (samodzielny plik python, is here). Istnieją dwie funkcje: fixed_hash_eq, która rozwiązuje twój problem, i compute_fixed_hash, który tworzy skrót ze struktury. Tests are here

Oto test:

obj1 = [1, 'd', {'a': 4, 'b': np.arange(10)}, (7, [1, 2, 3, 4, 5])] 
obj2 = [1, 'd', {'a': 4, 'b': np.arange(10)}, (7, [1, 2, 3, 4, 5])] 
obj3 = [1, 'd', {'a': 4, 'b': np.arange(10)}, (7, [1, 2, 3, 4, 5])] 
obj3[2]['b'][4] = 0 
assert fixed_hash_eq(obj1, obj2) 
assert not fixed_hash_eq(obj1, obj3)