【问题标题】:pythonic way to index list of objects索引对象列表的pythonic方法
【发布时间】:2016-09-26 02:44:19
【问题描述】:

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

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> 

如何设计一个简单的解决方案,以便每当我修改/删除/插入列表中的元素时,地图都会自动更新。我对优雅且可扩展的 pythonic 解决方案感兴趣。例如,将来,我应该能够轻松地添加另一个属性和 dict

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

【问题讨论】:

  • 当对象已经拥有这两个属性时,为什么还要创建&lt;status, object&gt;&lt;timestamp, object&gt; 的字典?
  • 我想要快速访问的索引,例如我想要获取状态为 3 的对象
  • 如果多个对象具有相似的状态或时间戳会怎样?
  • 我们可以将地图修改为 ...为简单起见,我们假设所有属性都不同
  • @jonrsharpe 推广到 O(n) 并不是很准确,这取决于数据。例如,考虑一些具有 O(log n) 查找的树结构。

标签: python dictionary indexing


【解决方案1】:

您可以在设置值时覆盖对象上的__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。
  • 您必须删除所有引用。这意味着将其从列表中删除并执行del obj1。此外,如果您在 python 提示符下执行此操作,最后返回的值将设置为 _ 变量,因此您还需要重置或删除它。
  • @darkknight 是的,你可以像 Yakym 那样做,只是让它们成为类属性,这样你就不必在类之外创建它们。
  • 再想一想……删除对象也很有意义。删除工作正常。
  • 再次不想为每个属性创建属性。所以想想我们是否可以进一步创建一个适用于任意数量属性的固定代码
【解决方案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 不需要它。
  • 不错!从列表中删除项目时不起作用。还添加了一个有问题的假设。所以也许 __delete_from_class 可以删除。
  • 你是对的,当从列表中删除对象时,这不会更新字典。 __delete_from_class 用于确保同一个对象没有被两个不同的值指向。
  • 如果你创建了weakref.WeakValueDictionary的索引,当你删除对象时它会自动删除它们
【解决方案3】:

要让集合意识到其元素的变化,元素和集合之间必须存在某种联系,当变化发生时可以进行通信。出于这个原因,我们要么必须将实例绑定到集合,要么代理集合的元素,以使变更通信不会泄漏到元素的代码中。

关于我将要介绍的实现的说明,代理方法仅在通过直接设置更改属性时才有效,而不是在方法内部。届时将需要一个更复杂的簿记系统。

此外,假设您需要使用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)

【讨论】:

    【解决方案4】:

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

    对象:

    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] 是列表 a,其中包含 ff.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)
    

    【讨论】:

      猜你喜欢
      • 2015-10-09
      • 2014-03-01
      • 2011-02-19
      • 1970-01-01
      • 2011-09-05
      • 2010-11-30
      • 2012-01-10
      • 1970-01-01
      • 2019-01-25
      相关资源
      最近更新 更多