Pythonic的方式索引对象列表

8

我有一个对象列表。每个对象都有两个字段。

obj1.status = 2
obj1.timestamp = 19211

obj2.status = 3
obj2.timestamp = 14211

obj_list = [obj1, obj2]

我将不断向列表中添加/删除对象,并更改对象的属性,例如我可能会将ob1.status更改为5。
现在我有两个字典:

dict1 - <status, object>
dict2 - <timestamp, object> 

我该如何设计一个简单的解决方案,以便在我修改/删除/插入列表元素时,地图可以自动更新。我感兴趣的是一种优雅且可扩展的Python解决方案。例如,在未来,我应该能够轻松地添加另一个属性和相应的字典。

此外,为了简单起见,让我们假设所有属性值都不同。例如,没有两个对象将具有相同的状态。


3
如果多个对象具有相似的状态或时间戳,会发生什么? - Vedang Mehta
我们可以修改地图为<状态,对象集合>... 为了简单起见,让我们假设所有属性都是不同的。 - dark knight
1
@jonrsharpe 一般化到O(n)并不是非常准确的,这取决于数据。例如考虑一些具有O(log n)查找的树结构。 - wim
@jonrsharpe 我想要在选定的属性上进行0(1)查找。将其视为内存中的关系型数据库,我们只需要在属性上进行快速查找(没有其他查询)。 - dark knight
好的,那你有什么呢?这是一个已经优化过的瓶颈还是一个过早的优化? - jonrsharpe
显示剩余8条评论
4个回答

2

这里的一种方法是为MyObj创建一个类级别的dict,并使用property装饰器定义更新行为。每次更改或添加对象时,都会反映在与类相关联的字典中。

编辑:正如@BrendanAbel所指出的那样,使用weakref.WeakValueDictionary代替dict可以处理从类级别字典中删除对象的情况。

from datetime import datetime
from weakref import WeakValueDictionary

DEFAULT_TIME = datetime.now()


class MyObj(object):
    """
    A sample clone of your object
    """
    timestamps = WeakValueDictionary()
    statuses   = WeakValueDictionary()

    def __init__(self, status=0, timestamp=DEFAULT_TIME):
        self._status    = status
        self._timestamp = timestamp

        self.status     = status
        self.timestamp  = timestamp

    def __update_class(self):
        MyObj.timestamps.update({self.timestamp: self})
        MyObj.statuses.update({self.status: self})

    def __delete_from_class(self):
        maybe_self = MyObj.statuses.get(self.status, None)
        if maybe_self is self is not None:
            del MyObj.statuses[self.status]

        maybe_self = MyObj.timestamps.get(self.timestamp, None)
        if maybe_self is self is not None:
            del MyObj.timestamps[self.timestamp]

    @property
    def status(self):
        return self._status

    @status.setter
    def status(self, val):
        self.__delete_from_class()
        self._status = val
        self.__update_class()

    @property
    def timestamp(self):
        return self._timestamp

    @timestamp.setter
    def timestamp(self, val):
        self.__delete_from_class()
        self._timestamp = val
        self.__update_class()

    def __repr__(self):
        return "MyObj: status={} timestamp={}".format(self.status, self.timestamp)


obj1 = MyObj(1)
obj2 = MyObj(2)
obj3 = MyObj(3)

lst = [obj1, obj2, obj3]

# In [87]: q.lst
# Out[87]: 
# [MyObj: status=1 timestamp=2016-05-27 13:43:38.158363,
#  MyObj: status=2 timestamp=2016-05-27 13:43:38.158363,
#  MyObj: status=3 timestamp=2016-05-27 13:43:38.158363]

# In [88]: q.MyObj.statuses[1]
# Out[88]: MyObj: status=1 timestamp=2016-05-27 13:43:38.158363

# In [89]: q.MyObj.statuses[1].status = 42

# In [90]: q.MyObj.statuses[42]
# Out[90]: MyObj: status=42 timestamp=2016-05-27 13:43:38.158363

# In [91]: q.MyObj.statuses[1]
# ---------------------------------------------------------------------------
# KeyError                                  Traceback (most recent call last)
# <ipython-input-91-508ab072bfc4> in <module>()
# ----> 1 q.MyObj.statuses[1]

# KeyError: 1

@darkknight 做得不错,尽管对于 Python 3 并非必需。 - hilberts_drinking_problem
不错!当从列表中删除项目时,它无法工作。还在问题中添加了一个假设。因此,也许可以删除 __delete_from_class。 - dark knight
您是正确的,当一个对象从列表中删除时,这并不会更新字典。__delete_from_class用于确保同一对象不被两个不同的值指向。 - hilberts_drinking_problem
1
如果你将索引设置为 weakref.WeakValueDictionary,当你删除对象时它会自动移除它们。 - Brendan Abel

2
你可以在对象上覆盖__setattr__来更新索引,每当你设置值的时候。你可以使用weakref字典来存储索引,这样当你删除对象并且不再使用它们时,它们会自动从索引中删除。
import weakref
from bunch import Bunch


class MyObject(object):

    indexes = Bunch()  # Could just use dict()

    def __init__(self, **kwargs):
        super(MyObject, self).__init__()
        for k, v in kwargs.items():
            setattr(self, k, v)

    def __setattr__(self, name, value):
        try:
            index = MyObject.indexes[name]
        except KeyError:
            index = weakref.WeakValueDictionary()
            MyObject.indexes[name] = index
        try:
            old_val = getattr(self, name)
            del index[old_val]
        except (KeyError, AttributeError):
            pass
        object.__setattr__(self, name, value)
        index[value] = self


obj1 = MyObject(status=1, timestamp=123123)
obj2 = MyObject(status=2, timestamp=2343)


print MyObject.indexes.status[1]
print obj1.indexes.timestamp[2343]
obj1.status = 5
print obj2.indexes['status'][5]

我在这里使用了一个 Bunch,因为它允许您使用 .name 表示法访问索引,但您也可以使用 dict 并使用 ['name'] 语法。


不错!删除功能无法正常工作。元素只从列表中删除,因此对象的强引用仍然存在于内存中。另外,如果能够将status_map和timestamp_map泛化,使得该类可以接受属性列表作为输入并从中构建,那就更好了。 - dark knight
你需要删除所有的引用。这意味着从列表中删除它并执行 del obj1。此外,如果你是在 Python 提示符下执行此操作,则最后返回的值将被设置为 _ 变量,因此你需要重置或删除它。 - Brendan Abel
@darkknight 是的,你可以像Yakym一样将它们作为类属性,这样你就不必在类外部创建它们了。 - Brendan Abel
再想一想...删除对象也是有意义的。删除操作已经生效了。 - dark knight
不想为每个属性都创建一个属性,所以正在考虑是否可以进一步创建一个固定的代码,可以适用于任意数量的属性。 - dark knight
显示剩余3条评论

1
为了使集合能够感知其元素的变化,元素和集合之间必须存在某种连接,可以在发生更改时进行通信。因此,我们要么将实例绑定到集合中,要么代理集合的元素,以便变化通信不会泄漏到元素的代码中。
关于我即将介绍的实现方法的说明,代理方法仅在通过直接设置更改属性时有效,而不是在方法内部。这将需要更复杂的簿记系统。
此外,它假设所有属性的精确副本不存在,因为您要求索引由 set 对象构建而不是 list。
from collections import defaultdict

class Proxy(object):
    def __init__(self, proxy, collection):
        self._proxy = proxy
        self._collection = collection

    def __getattribute__(self, name):
        if name in ("_proxy", "_collection"):
           return object.__getattribute__(self, name)
        else:
           proxy = self._proxy
           return getattr(proxy, name)

    def __setattr__(self, name, value):
        if name in ("_proxy", "collection"):
           object.__setattr__(self, name, value)
        else:
           proxied = self._proxy
           collection = self._collection
           old = getattr(proxied, name)
           setattr(proxy, name, value)
           collection.signal_change(proxied, name, old, value)


class IndexedCollection(object):
     def __init__(self, items, index_names):
         self.items = list(items)
         self.index_names = set(index_names)
         self.indices = defaultdict(lambda: defaultdict(set))

     def __len__(self):
         return len(self.items)

     def __iter__(self):
         for i in range(len(self)):
             yield self[i]    

     def remove(self, obj):
         self.items.remove(obj)
         self._remove_from_indices(obj)

     def __getitem__(self, i):
         # Ensure consumers get a proxy, not a raw object
         return Proxy(self.items[i], self)

     def append(self, obj):
         self.items.append(obj)
         self._add_to_indices(obj)

     def _add_to_indices(self, obj):
          for indx in self.index_names:
              key = getattr(obj, indx)
              self.indices[indx][key].add(obj)

     def _remove_from_indices(self, obj):
          for indx in self.index_names:
              key = getattr(obj, indx)
              self.indices[indx][key].remove(obj)

     def signal_change(self, obj, indx, old, new):
          if indx not in self.index_names:
               return
          # Tell the container to update its indices for a
          # particular attribute and object
          self.indices[indx][old].remove(obj)
          self.indices[indx][new].add(obj)

-1

我不确定这是否是您要求的,但是...

对象:

import operator
class Foo(object):
    def __init__(self):
        self.one = 1
        self.two = 2

f = Foo()
f.name = 'f'
g = Foo()
g.name = 'g'
h = Foo()
h.name = 'h'

name = operator.attrgetter('name')

列表: a 最初包含 fb 最初包含 h

a = [f]
b = [h]

字典:每个都有一个项目,值是其中一个列表

d1 = {1:a}
d2 = {1:b}

d1 [1] 是包含 f 的列表 a,而 f.one 的值为 1。

>>> d1
{1: [<__main__.Foo object at 0x03F4CA50>]}
>>> name(d1[1][0])
'f'
>>> name(d1[1][0]), d1[1][0].one
('f', 1)

修改 f.one 在字典中是可见的

>>> f.one = '?'
>>> name(d1[1][0]), d1[1][0].one
('f', '?')
>>> 

d2[1] 是列表 b,其中包含 h

>>> d2
{1: [<__main__.Foo object at 0x03F59070>]}
>>> name(d2[1][0]), d2[1][0].one
('h', 1)

将一个对象添加到b中,它会在字典中被看到

>>> b.append(g)
>>> b
[<__main__.Foo object at 0x03F59070>, <__main__.Foo object at 0x03F4CAF0>]
>>> d2
{1: [<__main__.Foo object at 0x03F59070>, <__main__.Foo object at 0x03F4CAF0>]}
>>> name(d2[1][1]), d2[1][1].one
('g', 1)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接