什么是子类化Python集合类的正确(或最佳)方法,添加新的实例变量?

时间:2009-04-28 15:05:21

标签: python subclass set instance-variables

我正在实现一个几乎与set相同的对象,但需要一个额外的实例变量,所以我将内置的set对象子类化。确保在复制其中一个对象时复制此变量的值的最佳方法是什么?

使用旧的set模块,以下代码完美运行:

import sets
class Fooset(sets.Set):
    def __init__(self, s = []):
        sets.Set.__init__(self, s)
        if isinstance(s, Fooset):
            self.foo = s.foo
        else:
            self.foo = 'default'
f = Fooset([1,2,4])
f.foo = 'bar'
assert( (f | f).foo == 'bar')

但使用内置设置模块无效。

我能看到的唯一解决方案是覆盖返回复制的set对象的每个方法...在这种情况下,我可能不会打扰子类化set对象。当然有一种标准的方法可以做到这一点吗?

(为了澄清,以下代码工作(断言失败):

class Fooset(set):
    def __init__(self, s = []):
        set.__init__(self, s)
        if isinstance(s, Fooset):
            self.foo = s.foo
        else:
            self.foo = 'default'

f = Fooset([1,2,4])
f.foo = 'bar'
assert( (f | f).foo == 'bar')

8 个答案:

答案 0 :(得分:16)

我最喜欢的方法来包装内置集合的方法:

class Fooset(set):
    def __init__(self, s=(), foo=None):
        super(Fooset,self).__init__(s)
        if foo is None and hasattr(s, 'foo'):
            foo = s.foo
        self.foo = foo



    @classmethod
    def _wrap_methods(cls, names):
        def wrap_method_closure(name):
            def inner(self, *args):
                result = getattr(super(cls, self), name)(*args)
                if isinstance(result, set) and not hasattr(result, 'foo'):
                    result = cls(result, foo=self.foo)
                return result
            inner.fn_name = name
            setattr(cls, name, inner)
        for name in names:
            wrap_method_closure(name)

Fooset._wrap_methods(['__ror__', 'difference_update', '__isub__', 
    'symmetric_difference', '__rsub__', '__and__', '__rand__', 'intersection',
    'difference', '__iand__', 'union', '__ixor__', 
    'symmetric_difference_update', '__or__', 'copy', '__rxor__',
    'intersection_update', '__xor__', '__ior__', '__sub__',
])

基本上你在自己的答案中做了同样的事情,但是用较少的loc。如果你想用列表和dicts做同样的事情,也很容易放入元类。

答案 1 :(得分:6)

我认为建议的方法不是直接从内置set继承子类,而是使用Abstract Base Class Set中提供的collections

使用ABC Set为您提供了一些免费的混合方法,因此您只需定义__contains__()__len__()__iter__()即可获得最小的Set类。如果你想要一些更好的设置方法,如intersection()difference(),你可能需要包装它们。

这是我的尝试(这个尝试类似于冻结,但您可以从MutableSet继承以获得可变版本):

from collections import Set, Hashable

class CustomSet(Set, Hashable):
    """An example of a custom frozenset-like object using
    Abstract Base Classes.
    """
    ___hash__ = Set._hash

    wrapped_methods = ('difference',
                       'intersection',
                       'symetric_difference',
                       'union',
                       'copy')

    def __repr__(self):
        return "CustomSet({0})".format(list(self._set))

    def __new__(cls, iterable):
        selfobj = super(CustomSet, cls).__new__(CustomSet)
        selfobj._set = frozenset(iterable)
        for method_name in cls.wrapped_methods:
            setattr(selfobj, method_name, cls._wrap_method(method_name, selfobj))
        return selfobj

    @classmethod
    def _wrap_method(cls, method_name, obj):
        def method(*args, **kwargs):
            result = getattr(obj._set, method_name)(*args, **kwargs)
            return CustomSet(result)
        return method

    def __getattr__(self, attr):
        """Make sure that we get things like issuperset() that aren't provided
        by the mix-in, but don't need to return a new set."""
        return getattr(self._set, attr)

    def __contains__(self, item):
        return item in self._set

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

    def __iter__(self):
        return iter(self._set)

答案 2 :(得分:4)

遗憾的是,set不遵循规则,并且__new__不会被调用来创建新的set对象,即使它们保留了类型。这显然是Python中的一个错误(问题#1721812,不会在2.x序列中修复)。如果不调用创建X对象的type对象,您永远不能获得X类型的对象!如果set.__or__不打算调用__new__,则正式有义务返回set个对象而不是子类对象。

但实际上,注意上面 nosklo 的帖子,你的原始行为没有任何意义。 Set.__or__运算符不应该重用任何一个源对象来构造它的结果,它应该是一个新的运算符,在这种情况下它的foo应该是"default"

所以,实际上,任何这样做 的人都必须重载这些运算符,以便他们知道foo的哪个副本被使用。如果它不依赖于组合的Foosets,你可以使它成为一个类默认值,在这种情况下它会得到尊重,因为新对象认为它属于子类类型。

我的意思是,如果您这样做,您的示例会有效:

class Fooset(set):
  foo = 'default'
  def __init__(self, s = []):
    if isinstance(s, Fooset):
      self.foo = s.foo

f = Fooset([1,2,5])
assert (f|f).foo == 'default'

答案 3 :(得分:2)

set1 | set2是一项不会修改现有set,但会返回新set的操作。创建并返回新的set。无法自动将set中的一个或两个的arbritary属性复制到新创建的set,而无法通过defining the __or__ method自行自定义|运算符。

class MySet(set):
    def __init__(self, *args, **kwds):
        super(MySet, self).__init__(*args, **kwds)
        self.foo = 'nothing'
    def __or__(self, other):
        result = super(MySet, self).__or__(other)
        result.foo = self.foo + "|" + other.foo
        return result

r = MySet('abc')
r.foo = 'bar'
s = MySet('cde')
s.foo = 'baz'

t = r | s

print r, s, t
print r.foo, s.foo, t.foo

打印:

MySet(['a', 'c', 'b']) MySet(['c', 'e', 'd']) MySet(['a', 'c', 'b', 'e', 'd'])
bar baz bar|baz

答案 4 :(得分:2)

看起来在c code中设置绕过__init__。但是,您将结束Fooset的实例,它只是没有机会复制该字段。

除了重写返回新集合的方法之外,我不确定在这种情况下你可以做多少。 Set显然是以一定的速度构建的,所以在c中做了很多工作。

答案 5 :(得分:1)

我试图回答以下问题:“如何使“set”运算符的返回值属于我的 set 子类的类型。忽略给定类的详细信息以及是否不是这个例子一开始就被打破了。我是从我自己的问题来到这里的,如果我的阅读是正确的,这将是重复的。

这个答案与其他一些答案的不同之处如下:

  • 给定的类(子类)只有通过添加装饰器才能改变
  • 因此足够通用,无需关心给定类的细节 (hasattr(s, 'foo'))
  • 额外费用是每个类(装饰时)支付一次,而不是每个实例。
  • 给定示例的唯一问题(特定于“set”)是方法列表,可以轻松定义。
  • 假设基类不是抽象的,可以自己复制构造(否则需要实现 __init__ 方法,从基类的实例复制)

库代码,可以放在项目或模块的任何地方:

class Wrapfuncs:
  def __init__(self, *funcs):
    self._funcs = funcs

  def __call__(self, cls):
    def _wrap_method(method_name):
      def method(*args, **kwargs):
          result = getattr(cls.__base__, method_name)(*args, **kwargs)
          return cls(result)
      return method

    for func in self._funcs:
      setattr(cls, func, _wrap_method(func))
    return cls

要将它与集合一起使用,我们需要返回一个实例的方法列表:

returning_ops_funcs = ['difference', 'symmetric_difference', '__rsub__', '__or__', '__ior__', '__rxor__', '__iand__', '__ror__', '__xor__', '__sub__', 'intersection', 'union', '__ixor__', '__and__', '__isub__', 'copy']

我们可以在我们的类中使用它:

@Wrapfuncs(*returning_ops_funcs)
class MySet(set):
  pass

关于这门课的特别之处,我不赘述。

我用以下几行测试了代码:

s1 = MySet([1, 2, 3])
s2 = MySet([2, 3, 4])
s3 = MySet([3, 4, 5])

print(s1&s2)
print(s1.intersection(s2))
print(s1 and s2)
print(s1|s2)
print(s1.union(s2))
print(s1|s2|s3)
print(s1.union(s2, s3))
print(s1 or s2)
print(s1-s2)
print(s1.difference(s2))
print(s1^s2)
print(s1.symmetric_difference(s2))

print(s1 & set(s2))
print(set(s1) & s2)

print(s1.copy())

打印:

MySet({2, 3})
MySet({2, 3})
MySet({2, 3, 4})
MySet({1, 2, 3, 4})
MySet({1, 2, 3, 4})
MySet({1, 2, 3, 4, 5})
MySet({1, 2, 3, 4, 5})
MySet({1, 2, 3})
MySet({1})
MySet({1})
MySet({1, 4})
MySet({1, 4})
MySet({2, 3})
{2, 3}
MySet({1, 2, 3})

有一种情况,结果不是最优的。这是,运算符与类的实例一起用作右手操作数,并将内置“set”的实例用作第一个。我不喜欢这个,但我相信这个问题在我见过的所有提议的解决方案中都很常见。

我也想过提供一个例子,其中使用了 collections.abc.Set。 虽然可以这样做:

from collections.abc import Set, Hashable
@Wrapfuncs(*returning_ops_funcs)
class MySet(set, Set):
  pass

我不确定它是否带来了@bjmc 所考虑的好处,或者它“免费”为您提供的“某些方法”是什么。 此解决方案旨在使用基类来完成工作并返回子类的实例。使用成员对象来完成工作的解决方案可能会以类似的方式生成。

答案 6 :(得分:0)

假设其他答案是正确的,并且覆盖所有方法是唯一的方法,这是我尝试以一种适度优雅的方式做到这一点。如果添加了更多实例变量,则只需要更改一段代码。不幸的是,如果将新的二元运算符添加到set对象中,此代码将会中断,但我认为没有办法避免这种情况。欢迎评论!

def foocopy(f):
    def cf(self, new):
        r = f(self, new)
        r.foo = self.foo
        return r
    return cf

class Fooset(set):
    def __init__(self, s = []):
        set.__init__(self, s)
        if isinstance(s, Fooset):
            self.foo = s.foo
        else:
            self.foo = 'default'

    def copy(self):
        x = set.copy(self)
        x.foo = self.foo
        return x

    @foocopy
    def __and__(self, x):
        return set.__and__(self, x)

    @foocopy
    def __or__(self, x):
        return set.__or__(self, x)

    @foocopy
    def __rand__(self, x):
        return set.__rand__(self, x)

    @foocopy
    def __ror__(self, x):
        return set.__ror__(self, x)

    @foocopy
    def __rsub__(self, x):
        return set.__rsub__(self, x)

    @foocopy
    def __rxor__(self, x):
        return set.__rxor__(self, x)

    @foocopy
    def __sub__(self, x):
        return set.__sub__(self, x)

    @foocopy
    def __xor__(self, x):
        return set.__xor__(self, x)

    @foocopy
    def difference(self, x):
        return set.difference(self, x)

    @foocopy
    def intersection(self, x):
        return set.intersection(self, x)

    @foocopy
    def symmetric_difference(self, x):
        return set.symmetric_difference(self, x)

    @foocopy
    def union(self, x):
        return set.union(self, x)


f = Fooset([1,2,4])
f.foo = 'bar'
assert( (f | f).foo == 'bar')

答案 7 :(得分:-2)

对我来说,这在Win32上使用Python 2.5.2非常有效。使用您的类定义和以下测试:

f = Fooset([1,2,4])
s = sets.Set((5,6,7))
print f, f.foo
f.foo = 'bar'
print f, f.foo
g = f | s
print g, g.foo
assert( (f | f).foo == 'bar')

我得到了这个输出,这是我所期望的:

Fooset([1, 2, 4]) default
Fooset([1, 2, 4]) bar
Fooset([1, 2, 4, 5, 6, 7]) bar