子类化列表

4 投票
1 回答
1914 浏览
提问于 2025-04-15 12:07

我想创建一个叫做DataSet的类,简单来说就是一个样本的列表。 不过,我需要在往这个DataSet里添加样本的时候,做一些特别的处理。

有没有什么简单的方法可以做到这一点,而不需要自己去写append、extend、iadd等这些方法呢?

更新:我想给每个样本加一个指针,指向它在DataSet中的位置。这是我用的处理算法所需要的。我有一个解决方案,但感觉不太优雅——就是一个renumber()函数,它确保这些指针是有效的。

1 个回答

5

我不知道有没有办法做到你想要的——在不重写变更方法的情况下重写它们。不过,使用类装饰器,你可以“自动化”重写的版本(假设每个都可以通过包装基类中的相应方法来实现),所以也不是太麻烦……

比如说,你想做的事情是添加一个“已修改”标志,如果数据自上次调用.save(这是你用来保存数据并将self.modified设置为False的方法)以来可能已经改变,这个标志就为真。

那么……:

def wrapMethod(cls, n):
    f = getattr(cls, n)
    def wrap(self, *a):
      self.dirty = True
      return f(self, *a)
    return wrap

def wrapListMutators(cls):
  for n in '''__setitem__ __delitem__ __iadd__ __imul__
              append extend insert pop remove reverse sort'''.split():
    f = wrapMethod(cls, n)
    setattr(cls, n, f)
  return cls

@wrapListMutators
class DataSet(list):
  dirty = False
  def save(self): self.dirty = False

这种语法需要Python 2.6或更高版本,但在早期的Python版本中(那些只支持在def语句上使用装饰器,而不支持在class语句上使用装饰器的版本,甚至是一些非常旧的完全不支持装饰器的版本),你只需要更改最后一部分(class语句),变成:

class DataSet(list):
  dirty = False
  def save(self): self.dirty = False
DataSet = wrapListMutators(DataSet)

换句话说,漂亮的装饰器语法其实就是在普通函数调用上加了一点语法糖,这个函数以类作为参数并重新分配它。

编辑:现在你已经编辑了你的问题,明确了你的具体需求——在每个项目上维护一个字段bp,使得对于所有itheset[i].bp == i——这样更容易权衡各种方法的利弊。

你可以调整我之前提到的方法,但在调用被包装的方法之前,不要给self.dirty赋值,而是在之后调用self.renumber(),也就是说:

def wrapMethod(cls, n):
    f = getattr(cls, n)
    def wrap(self, *a):
      temp = f(self, *a)
      self.renumber()
      return temp
    return wrap

这样可以满足你所说的要求,但在很多情况下,它会做比必要的更多的工作:例如,当你append一个项目时,这会不必要地“重新编号”所有现有的项目(变成它们已经有的值)。但是,任何完全自动化的方法怎么能“知道”哪些项目需要重新计算.bp,而不需要O(N)的努力呢?至少它必须查看每一个项目(因为你不想单独编写,比如appendinsert等),这已经是O(N)了。

所以,只有在每次对列表的更改都可以接受O(N)的情况下,这种方法才是可以接受的(基本上只有在列表总是保持较小和/或不经常更改的情况下)。

一个更有前景的想法可能是,不要一直维护.bp值,而是在需要的时候“及时”维护。让bp成为一个(只读)属性,调用一个方法来检查容器是否“脏”(容器中的“脏”标志是使用我之前给出的自动化代码维护的),然后再重新编号容器(并将其“脏”属性设置为False)。

当列表通常经历一阵变化时,这种方法会很好用,只有在那时你才需要访问项目的bp,然后又是一阵变化,等等。在现实世界的容器中,这种变化和读取交替并不少见,但只有你知道这是否适用于你的具体情况!

要想获得更好的性能,我认为你需要在这个通用方法的基础上做一些手动编码,以利用频繁的特殊情况。例如,append可能会被非常频繁地调用,而在特殊情况下append所需的工作量非常小,所以写那两三行代码(在这种情况下不设置脏位)可能是值得的。

有一个警告:如果列表中有任何项目出现两次,那么任何方法都将无法工作(实际上你的要求变得自相矛盾)——这当然是完全可能的,除非你采取措施避免这种情况(你可以在renumber中轻松诊断出来——通过保持一个已经看到的元素集合,并在发现重复时抛出异常——如果这对你来说还不算太晚;如果你要求在导致重复的变更发生时进行“实时”诊断,那就更难了)。也许你可以放宽你的要求,这样如果某个项目出现两次也没关系,bp可以只指示其中一个索引;或者将bp变成一个元素出现的索引的集合(这也可以为从不在列表中的元素获取bp提供一种平滑的方法)。等等等等;我建议你深入考虑(并记录!)所有这些边缘情况——正确性优先于性能!

撰写回答