2015-04-17 3 views
5

Python 2.7 docs утверждает, что assertItemsEqual "является эквивалентом assertEqual(sorted(expected), sorted(actual))". В приведенном ниже примере все тесты проходят, за исключением test4. Почему в этом случае ошибка assertItemsEqual?Почему успешный assertEqual не всегда означает успешный assertItemsEqual?

В соответствии с принципом наименьшего удивления, учитывая два итерационных, я ожидал бы, что успешный assertEqual влечет за собой успешный 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() 

Вот результат на моей машине:

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

Поскольку вы не определили порядок на объектах 'foo'? – wim

+0

Спасибо. К вашему моменту, test4 проходит, если я определяю метод __hash__ на 'foo'. Однако в документах указано, что assertItemsEqual работает с неотображаемыми объектами. Неужели я не понимаю документа? –

+0

Я не очень хорошо знаю Python, но сообщение об ошибке ясно говорит вам, что оно сравнивает списки, подсчитывая уникальные объекты в обоих, а затем сравнивая ключ подсчета по ключу. Сравнение осуществляется по адресу объекта. Поскольку у вас есть разные экземпляры объектов в каждом списке, списки сравниваются как не равные. Если вы сказали «a = foo (1); b = foo (2); self.list1 = [a, b] self.list2 = [b, a] ', я уверен, что последний тест пройдет. – Gene

ответ

3

Спецификация документа интересно отделяется от реализации, которая никогда не делает никакой сортировки. Here is the source code. Как вы можете видеть, сначала он пытается подсчитать путем хеширования с использованием collections.Counter. Если это не удается с ошибкой типа (поскольку в одном из списков содержится элемент, который не сотрясается), он перемещается на a second algorithm, где он сравнивается с использованием питов == и O (n^2).

Так что если ваш класс foo был раскаленным, второй алгоритм сигнализировал бы совпадение. Но это прекрасно хешируется. Из документов:

Объекты, являющиеся экземплярами пользовательских классов, по умолчанию хешируются; все они сравнивают неравные (кроме самих себя), и их хэш-значение получается из их id().

Я проверил это, позвонив по телефону collections.Counter([foo(1)]). Исключение ошибки типа.

Так вот, где ваш код сходит с рельсов.Из документов на __hash__:

, если она определяет CMP() или эк(), но не хэша(), его экземпляры не будут использоваться в хэшированной коллекции.

К сожалению, «непригодный к употреблению», по-видимому, не приравнивается к «нерасходуемым».

Он продолжает говорить:

Классы, наследуемые метод хэш() из родительского класса, но изменить значение имп() или эк() такой, что хэш Возвращаемое значение больше не подходит (например, путем перехода к концепции равенства, основанной на значении, вместо равенства по умолчанию, основанного на идентичности) может явно обозначать себя как нечувствительный, установив hash = Нет в определении класса.

Если переопределить:

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 

все тесты проходят!

Таким образом, похоже, что документы не совсем ошибочны, но они также не слишком ясны. Они должны упомянуть, что подсчет выполняется с хешированием, и только если это не удается, простое совпадение равенства. Это только действительный подход, если объекты имеют либо полную хеширующую семантику, либо полностью расставлены. Твои были в средней земле. (Я считаю, что Python 3 более строг в отношении запрета или, по крайней мере, предупреждения против классов этого типа.)

+0

Спасибо, Джин. Ваш ответ четко написан и объясняет основную причину. Я особенно ценю, что вы обосновали свой ответ в исходном коде Python. –

+0

Добро пожаловать. Это хорошо написанный вопрос. Следующая интересная вещь заключается в том, что я попытался установить '__hash__ = None', как рекомендовано для пользовательских классов, устанавливающих' __eq__', но не '__hash__'. Разумеется, второй алгоритм работает, потому что это делает неуправляемым foo. Но, видимо, у него есть ошибка! Я получаю исключение, поле 'a' не существует. Там сравнивается фиктивный объект. Мне нужно немного поспать. Получайте удовольствие от этого. – Gene

+0

@MatthewNizol Я понял, что происходит. Второй алгоритм работает, записывая ссылки на объект NULL по уже сопоставленным. Ваш '__eq__' терпел неудачу в этих ссылках, потому что у NULL не было поля' a'. Я исправил его в коде выше. Доброй ночи! – Gene

3

Соответствующая часть документации здесь:

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

Большинство других объектов встроенных типов сравнивать неравномерно, если они не являются одним и тем же объектом; выбор того, считается ли один объект меньшим или больше другого, производится произвольно, но последовательно в течение одного выполнения программы.

Так что, если вы сделаете x, y = foo(1), foo(1), то это не очень хорошо определены, заканчивается ли упорядоченность в качестве x > y или x < y. В python3 вам не разрешат вообще, вызов sorted должен вызвать исключение.

Поскольку для каждого метода тестирования unittest вызывает setUp, вы получаете разные экземпляры foo, создаваемые каждый раз.


assertItemsEqual осуществляется с collections.Counter (подкласс Словаре), так что я думаю, что отказ test4 может быть симптомом этого факта:

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

Если два элемента считаются равными, то они должны иметь то же самое, в противном случае вы рискуете сломать сопоставления, подобные этому.

+0

Спасибо, wim. Я вижу, что вызов sorted() может приводить к неопределенному поведению. Тем не менее, я тестировал поведение после добавления метода __cmp__ для foo (возвращает -1, если self.a other.a), и test4 все еще не работает. Test4 успешно работает, если добавить метод __hash__ (return self.a); поэтому кажется, что assertItemEqual группируется на основе хэш-значения.Однако документы не указывают на это требование. –

+0

Я думаю, это потому, что 'assertItemsEqual' реализован с помощью' collections.Counter'. Интересно. Позвольте мне исследовать далее .. – wim

+0

Большое спасибо за исследование. Вы пришли к такому же выводу, что Джин сделал ... что основной причиной является использование 'collections.Counter' в реализации. Однако, поскольку я могу только принять 1 ответ, я принял Gene из-за его дополнительного наблюдения, что все пользовательские объекты по умолчанию хешируются с помощью своего id(), который разъясняет, почему 'collections.Counter' группирует экземпляры foo так, как он есть. Еще раз спасибо! –

Смежные вопросы