【问题标题】:What is the correct (or best) way to subclass the Python set class, adding a new instance variable?对 Python 集类进行子类化、添加新实例变量的正确(或最佳)方法是什么?
【发布时间】:2010-10-22 08:34:22
【问题描述】:

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

使用旧的 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')

)

【问题讨论】:

    标签: python subclass set instance-variables


    【解决方案1】:

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

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

    【讨论】:

    • 这是一个有用的贡献,谢谢。通过使 _wrap_methods 成为类方法而不是函数,您看起来并没有获得太多收益 - 这纯粹是因为它提供的模块化吗?
    【解决方案2】:

    我认为推荐的方法不是直接从内置的set 子类化,而是利用collections.abc 中的Abstract Base Class Set

    使用 ABC Set 为您提供了一些免费的混合方法,因此您可以通过仅定义 __contains__()__len__()__iter__() 来拥有一个最小的 Set 类。如果您想要一些更好的 set 方法,例如 intersection()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)
    

    【讨论】:

    • 请注意isinstance(CustomSet(), set) == False 不起作用。由于我不知道的原因,除非您从 set 继承,否则无法使其工作。
    • 从历史上看,Python 更喜欢“鸭子类型”并且倾向于不鼓励显式的isinstance() 检查。您的观点是正确的,但我怀疑在现实生活中您很少会遇到具有类似集合接口的对象不足而您需要“真实”set 实例的情况。
    • ___hash__ 是否有额外的_
    • @Mr_and_Mrs_D,好眼力!那是一个错字:魔术方法是__hash__
    【解决方案3】:

    遗憾的是,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'
    

    【讨论】:

      【解决方案4】:

      set1 | set2 是一个不会修改现有set 的操作,而是返回一个新的set。新的set 被创建并返回。如果不通过defining the __or__ method 自己自定义| 运算符,则无法使其自动将任意属性从sets 之一或两者复制到新创建的set

      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
      

      【讨论】:

      • 这就是我所怀疑的。在这种情况下,我将不得不覆盖 andorrandrorrsub rxorsubxor、add、copy、difference、intersection、symmetric_difference 和 union。我错过了吗?老实说,我一直在寻找具有我上面列出的 2.5 解决方案的简单通用性的东西......但是否定的答案也很好。在我看来,它确实有点像一个错误。
      【解决方案5】:

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

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

      【讨论】:

      • 叹息。谢谢。那也是我对 C 代码的阅读,但我是 python 新手,所以认为值得一问。我忘记了我一般不喜欢子类化的原因——“外部”子类变得依赖于其超类未发布的内部实现细节。
      【解决方案6】:

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

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

      • 给定的类(子类)只能通过添加装饰器来改变
      • 因此足够笼统,无需关心给定类的细节 (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
      

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

      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 想到的好处,或者它“免费”为您提供的“某些方法”是什么。 该解决方案旨在使用基类来完成工作并返回子类的实例。可能会以类似的方式生成使用成员对象完成工作的解决方案。

      【讨论】:

        【解决方案7】:

        假设其他答案是正确的,并且重写所有方法是做到这一点的唯一方法,这是我尝试以一种适度优雅的方式做到这一点。如果添加更多实例变量,则只需更改一段代码。不幸的是,如果将新的二元运算符添加到 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')
        

        【讨论】:

        • 复制方法中有一些无限递归。 x = self.copy() 应该是 x = super(Fooset,self).copy()
        • 是的,你是对的。使用 super() 是否比明确提及超类更好?
        【解决方案8】:

        对我来说,在 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
        

        【讨论】:

        • 是的,这适用于 2.5.2,但你能否让它适用于 python 2.6 中的内置 set 类型?
        • 因为您的代码中有import sets,并且没有提到 2.6,所以我假设您将使用 sets.py 模块。如果这在 2.6 中不再可用,您可能会不走运
        猜你喜欢
        • 2015-04-26
        • 1970-01-01
        • 1970-01-01
        • 2013-08-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-24
        相关资源
        最近更新 更多