18

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

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

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 模块。

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

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

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')

)

4

8 回答 8

20

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

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 更少。如果您也想对列表和字典做同样的事情,那么放入元类也很容易。

于 2009-04-30T01:01:00.863 回答
11

我认为这样做的推荐方法不是直接从 built-in 子类化set,而是利用collections.abc中可用的Abstract Base ClassSet

使用 ABC Set 为您提供了一些免费的混合方法,因此您可以通过仅定义 和 来拥有一个最小的__contains__()Set__len__()__iter__()。如果您想要一些更好的 set 方法,例如intersection()and difference(),您可能必须将它们包装起来。

这是我的尝试(这恰好是一个frozenset-like,但你可以继承MutableSet来获得一个可变版本):

from collections.abc 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=None):
        selfobj = super(CustomSet, cls).__new__(CustomSet)
        selfobj._set = frozenset() if iterable is None else 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)
于 2011-07-14T19:15:53.363 回答
4

可悲的是, set 不遵循规则并且__new__不会被调用来创建新set对象,即使它们保留了类型。这显然是 Python 中的一个错误(问题#1721812,不会在 2.x 序列中修复)。type如果不调用创建 X 对象的对象,您将永远无法获得 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'
于 2012-09-07T13:53:11.933 回答
2

set1 | set2是一个不会修改任何现有的操作set,而是返回一个新的set。新set的被创建并返回。没有办法让它自动将任意属性从一个或两个sets 复制到新创建的set,除非通过定义 方法|自己自定义运算符。__or__

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
于 2009-04-28T15:29:24.273 回答
2

它看起来像__init__c 代码中设置绕过。但是,您将结束 的实例Fooset,它只是没有机会复制该字段。

除了覆盖返回新集合的方法之外,我不确定在这种情况下你能做太多事情。Set 显然是为一定的速度而构建的,所以在 c 中做了很多工作。

于 2009-04-28T15:59:59.097 回答
1

我试图回答阅读它的问题:“我怎样才能使“集合”的运算符的返回值成为我的集合子类的类型。忽略给定类的详细信息以及示例是否一开始就坏了。如果我的阅读正确,我是从我自己的问题来到这里的,这将是重复的。

此答案与其他一些答案不同,如下所示:

  • 给定的类(子类)只能通过添加装饰器来改变
  • 因此足够笼统,不关心给定类的细节(hasattr(s,'foo'))
  • 额外的费用是每班支付一次(当它被装饰时),而不是每次都支付。
  • 给定示例的唯一问题是特定于“集合”的方法列表,可以轻松定义。
  • 假设基类不是抽象的,可以自己复制构造(否则需要实现 __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 想到的好处,或者“一些方法”是什么,它“免费”为您提供。该解决方案旨在使用基类来完成工作并返回子类的实例。一个使用成员对象来完成工作的解决方案可能会以类似的方式生成。

于 2021-01-27T15:25:08.800 回答
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')
于 2009-04-28T16:31:30.177 回答
-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
于 2009-04-28T15:39:34.263 回答