Поведение объекта в заданных операциях
Я пытаюсь создать пользовательский объект, который корректно ведет себя в заданных операциях.
Как правило, это работает, но я хочу убедиться, что полностью понимаю последствия. В частности, меня интересует поведение, когда в объекте есть дополнительные данные, которые не включены в методы равенства/хэша. Кажется, что в операции "пересечения" он возвращает набор объектов, которые сравниваются, когда операции "union" возвращают набор сравниваемых объектов.
Чтобы проиллюстрировать:
class MyObject:
def __init__(self,value,meta):
self.value = value
self.meta = meta
def __eq__(self,other):
return self.value == other.value
def __hash__(self):
return hash(self.value)
a = MyObject('1','left')
b = MyObject('1','right')
c = MyObject('2','left')
d = MyObject('2','right')
e = MyObject('3','left')
print a == b # True
print a == c # False
for i in set([a,c,e]).intersection(set([b,d])):
print "%s %s" % (i.value,i.meta)
#returns:
#1 right
#2 right
for i in set([a,c,e]).union(set([b,d])):
print "%s %s" % (i.value,i.meta)
#returns:
#1 left
#3 left
#2 left
Является ли это поведение документированным где-то и детерминированным? Если да, то каков руководящий принцип?
Ответы
Ответ 1
Нет, он не детерминирован. Проблема в том, что вы нарушили equals и хэш-инвариант, что два объекта эквивалентны, когда они равны. Исправьте свой объект, не пытайтесь быть умным и злоупотреблять тем, как работает реализация. Если метазначение является частью идентификатора MyObject, оно должно быть включено в eq и hash.
Вы не можете полагаться на заданное пересечение, чтобы следовать за любым порядком, поэтому нет возможности легко делать то, что вы хотите. То, что вы в итоге делаете, - это пересечение только по значению, а затем просмотрите все свои объекты для более старого, чтобы заменить его, для каждого. Никакой хороший способ сделать это алгоритмически.
Союзы не так уж плохи:
##fix the eq and hash to work correctly
class MyObject:
def __init__(self,value,meta):
self.value = value
self.meta = meta
def __eq__(self,other):
return self.value, self.meta == other.value, other.meta
def __hash__(self):
return hash((self.value, self.meta))
def __repr__(self):
return "%s %s" % (self.value,self.meta)
a = MyObject('1','left')
b = MyObject('1','right')
c = MyObject('2','left')
d = MyObject('2','right')
e = MyObject('3','left')
union = set([a,c,e]).union(set([b,d]))
print union
#set([2 left, 2 right, 1 left, 3 left, 1 right])
##sort the objects, so that older objs come before the newer equivalents
sl = sorted(union, key= lambda x: (x.value, x.meta) )
print sl
#[1 left, 1 right, 2 left, 2 right, 3 left]
import itertools
##group the objects by value, groupby needs the objs to be in order to do this
filtered = itertools.groupby(sl, lambda x: x.value)
##make a list of the oldest (first in group)
oldest = [ next(group) for key, group in filtered]
print oldest
#[1 left, 2 left, 3 left]
Ответ 2
Заказ не имеет значения:
>>> [ (u.value, u.meta) for u in set([b,d]).intersection(set([a,c,e])) ]
[('1', 'right'), ('2', 'right')]
>>> [ (u.value, u.meta) for u in set([a,c,e]).intersection(set([b,d])) ]
[('1', 'right'), ('2', 'right')]
Однако, если вы это сделаете:
>>> f = MyObject('3', 'right')
И добавьте f
в "правильный" набор:
>>> [ (u.value, u.meta) for u in set([a,c,e]).intersection(set([b,d,f])) ]
[('1', 'right'), ('3', 'right'), ('2', 'right')]
>>> [ (u.value, u.meta) for u in set([b,d,f]).intersection(set([a,c,e])) ]
[('1', 'left'), ('3', 'left'), ('2', 'left')]
Итак, вы можете видеть, что поведение зависит от размера наборов (тот же эффект возникает, если вы union
). Это может зависеть и от других факторов. Я думаю, что вы охотитесь за источником python, если хотите узнать почему.
Ответ 3
Скажем, ваши объекты имеют два разных типа атрибутов: атрибуты ключей и атрибуты данных. В вашем примере MyObject.value
является ключевым атрибутом.
Храните все свои объекты в качестве значений в словаре, с ключевыми атрибутами, чтобы убедиться, что в словаре введен только ваш предпочтительный (например, самый старый временной интервал). Выполняйте операции с тем же ключом, который используется в словаре, и извлекайте фактические объекты из словаря:
result= [dict1[k] for k in set_operation_result]