Upd.2: я начинаю сомневаться, что
* * *
Follow-up отсюда http://morfizm.livejournal.com/471580.html
Баг состоял в том, что result = self.tags устанавливает результат на ссылку на поле tags у узла дерева, таким образом, последующее изменение result непреднамеренно портит дерево. (Фикс выглядел бы, например, так: result = set(self.tags) - это создало бы копию множества).
Причём баг особо нехороший, потому что проявляется он только при определённом стечении обстоятельств, до которого трудно догадаться во время тестирования, если не знать про этот класс багов и специально их не ловить:
1. Дерево должно быть таким, чтобы операторы result = result | ... ни разу не выполнились - т.к. бинарная операция | создаёт копию множества, которую можно впоследствии безопасно изменять.
2. result нужно таки поменять - например, вызывать обработку нескольких деревьев, объединяя результаты:
result = t1.evaluate_tree(item)
result |= t2.evaluate_tree(item) // here we will sometimes edit t1.tags via "result" name
(операция |= будет редактировать left-side множество in-place, не создавая промежуточного объекта)
3. Вышеописанный код может испортить дерево t1, но баг проявится только когда этот код (или другой, использующий t1) будет вызван повторно.
http://zephyrfalcon.org/labs/python_pitfalls.html
и прошёлся по пунктам :) Второй пункт - "Assignment, aka names and objects" - как раз про это.
Остальные пытались искать алгоритмические баги (которых не было), а также выявляли несовершенство моего примера, который не обладал достаточной полнотой, чтобы упомянутый выше баг был единственно возможным.
Вообще, это очень интересный класс багов - когда не понятно, можешь ли ты безопасно пользоваться возвращаемым значением (в данном случае, изменять его). На самом деле, похожий баг я видел C++, когда использовался XML-парсер FAXPP. Для эффективности он возвращает строку в виде указателя на свой внутренний буфер. Баг состоял в том, что эту строку не скопировали, а просто запомнили указатель, потом вызвали фукнцию, которая где-то в глубине опять вызывала парсер, менявший буфер, потом вызываемая функция пыталась использовать запомненную строку, рассчитывая, что буфер не изменился. Было вроде такого:
FAXPP_TEXT* mytoken = faxpp_get_string()
call_something() // something will call faxpp_get_string() again, destroying the buffer
... // try using mytoken, which has wrong value.