Структура данных Python для коллекции объектов с произвольным доступом на основе атрибута
Мне нужна коллекция объектов, которую можно найти по определенному (уникальному) атрибуту, общему для каждого из объектов. Прямо сейчас я использую словарь, присваивающий словарный ключ атрибуту. Вот пример того, что я имею сейчас:
class Item():
def __init__(self, uniq_key, title=None):
self.key = uniq_key
self.title = title
item_instance_1 = Item("unique_key1", title="foo")
item_instance_2 = Item("unique_key3", title="foo")
item_instance_3 = Item("unique_key2", title="foo")
item_collection = {
item_instance_1.key: item_instance_1,
item_instance_2.key: item_instance_2,
item_instance_3.key: item_instance_3
}
item_instance_1.key = "new_key"
Теперь это кажется довольно громоздким решением, так как ключ не является ссылкой на атрибут, но принимает значение атрибута ключа при присваивании, что означает, что:
- ключи словаря дублируют информацию, уже представленную в форме атрибута объекта, и
- при изменении атрибута объекта ключ словаря не обновляется.
Использование списка и повторение объекта кажется еще более неэффективным.
Итак, есть ли более подходящая структура данных, чем dict для этого конкретного случая, коллекция объектов, предоставляющая мне произвольный доступ на основе определенного атрибута объекта?
Это должно работать с Python 2.4, так как это то, что я застрял (на работе).
Если это не было очевидно, я новичок в Python.
4 ответа
На самом деле, как вы боитесь, дублирования информации не существует: ключ диктата и объект .key
атрибут, это всего лишь две ссылки на один и тот же объект.
Единственная реальная проблема - "что если .key
переназначается ". Ну, тогда, очевидно, вы должны использовать свойство, которое обновляет все релевантные диктанты, а также атрибут экземпляра; поэтому каждый объект должен знать все диктовки, в которых он может быть зарегистрирован. В идеале нужно использовать слабые ссылки для цель, чтобы избежать круговых зависимостей, но, увы, вы не можете взять weakref.ref
(или прокси) к диктату. Итак, я использую здесь нормальные ссылки (альтернатива не использовать dict
экземпляры, но, например, какой-то особый подкласс - не удобно).
def enregister(d, obj):
obj.ds.append(d)
d[obj.key] = obj
class Item(object):
def __init__(self, uniq_key, title=None):
self._key = uniq_key
self.title = title
self.ds = []
def adjust_key(self, newkey):
newds = [d for d in self.ds if self._key in d]
for d in newds:
del d[self._key]
d[newkey] = self
self.ds = newds
self._key = newkey
def get_key(self):
return self._key
key = property(get_key, adjust_key)
Редактировать: если вы хотите одну коллекцию со ВСЕМИ экземплярами Item, это еще проще, поскольку вы можете сделать коллекцию атрибутом уровня класса; действительно, это может быть WeakValueDictionary, чтобы избежать ошибочного сохранения элементов в живых, если это то, что вам нужно. То есть:
class Item(object):
all = weakref.WeakValueDictionary()
def __init__(self, uniq_key, title=None):
self._key = uniq_key
self.title = title
# here, if needed, you could check that the key
# is not ALREADY present in self.all
self.all[self._key] = self
def adjust_key(self, newkey):
# "key non-uniqueness" could be checked here too
del self.all[self._key]
self.all[newkey] = self
self._key = newkey
def get_key(self):
return self._key
key = property(get_key, adjust_key)
Теперь вы можете использовать Item.all['akey']
, Item.all.get('akey')
, for akey in Item.all:
и так далее - весь богатый функционал диктов.
Есть много замечательных вещей, которые вы можете сделать здесь. Один из примеров - позволить классу отслеживать все:
class Item():
_member_dict = {}
@classmethod
def get_by_key(cls,key):
return cls._member_dict[key]
def __init__(self, uniq_key, title=None):
self.key = uniq_key
self.__class__._member_dict[key] = self
self.title = title
>>> i = Item('foo')
>>> i == Item.get_by_key('foo')
True
Обратите внимание, что вы сохраните проблему обновления: если key
изменения, _member_dict
не синхронизируется. Вот где инкапсуляция пригодится: сделать (практически) невозможным изменить key
без обновления словаря. Для хорошего учебника о том, как это сделать, см. Этот учебник.
Редактирование, чтобы исправить проблему, которая у меня возникла - из-за моего параметра по умолчанию "collection = dict()" (*bonk*).
Теперь каждый вызов функции будет возвращать класс со своей собственной коллекцией, как предполагалось - это для удобства на случай, если потребуется более одной такой коллекции. Также я помещаю коллекцию в класс и просто возвращаю класс вместо двух отдельно в кортеже, как раньше. (Оставив контейнер по умолчанию здесь как dict(), но его можно заменить на WeakValueDictionary Алекса, что, конечно, очень здорово.)
def make_item_collection(container = None):
''' Create a class designed to be collected in a specific collection. '''
container = dict() if container is None else container
class CollectedItem(object):
collection = container
def __init__(self, key, title=None):
self.key = key
CollectedItem.collection[key] = self
self.title = title
def update_key(self, new_key):
CollectedItem.collection[
new_key] = CollectedItem.collection.pop(self.key)
self.key = new_key
return CollectedItem
# Usage Demo...
Item = make_item_collection()
my_collection = Item.collection
item_instance_1 = Item("unique_key1", title="foo1")
item_instance_2 = Item("unique_key2", title="foo2")
item_instance_3 = Item("unique_key3", title="foo3")
for k,v in my_collection.iteritems():
print k, v.title
item_instance_1.update_key("new_unique_key")
print '****'
for k,v in my_collection.iteritems():
print k, v.title
И вот вывод в Python 2.5.2:
unique_key1 foo1
unique_key2 foo2
unique_key3 foo3
****
new_unique_key foo1
unique_key2 foo2
unique_key3 foo3
Ну, Дикт действительно то, что вы хотите. Громоздким может быть не сам диктат, а то, как вы его строите. Вот небольшое улучшение вашего примера, показывающее, как использовать выражение списка и конструктор dict, чтобы легко создать ваш dict поиска. Здесь также показано, как создать многопользовательский тип dict, чтобы искать совпадающие элементы по заданному значению поля, которое может быть дублировано между элементами:
class Item(object):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __str__(self):
return str(self.__dict__)
def __repr__(self):
return str(self)
allitems = [
Item(key="red", title="foo"),
Item(key="green", title="foo"),
Item(key="blue", title="foofoo"),
]
# if fields are unique
itemByKey = dict([(i.key,i) for i in allitems])
# if field value can be duplicated across items
# (for Python 2.5 and higher, you could use a defaultdict from
# the collections module)
itemsByTitle = {}
for i in allitems:
if i.title in itemsByTitle:
itemsByTitle[i.title].append(i)
else:
itemsByTitle[i.title] = [i]
print itemByKey["red"]
print itemsByTitle["foo"]
Печать:
{'key': 'red', 'title': 'foo'}
[{'key': 'red', 'title': 'foo'}, {'key': 'green', 'title': 'foo'}]