Структура данных 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'}]
Другие вопросы по тегам