簡體   English   中英

子類化 Python 集合類、添加新實例變量的正確(或最佳)方法是什么?

[英]What is the correct (or best) way to subclass the Python set class, adding a new instance variable?

我正在實現一個與集合幾乎相同的對象,但需要一個額外的實例變量,所以我將內置的集合對象子類化。 確保在復制我的對象之一時復制此變量的值的最佳方法是什么?

使用舊的 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 對象的方法......在這種情況下,我最好不要打擾 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')

)

我最喜歡的包裝內置集合方法的方法:

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

我認為推薦的方法不是直接從內置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)

可悲的是, set 不遵循規則並且__new__不會被調用來創建新的set對象,即使它們保留了類型。 這顯然是 Python 中的一個錯誤(問題#1721812 ,不會在 2.x 序列中修復)。 如果不調用創建 X 對象的type對象,就永遠不能獲得 X type對象! 如果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'

set1 | set2 set1 | set2是一個不會修改任何現有set ,而是返回一個新的set set被創建並返回。 沒有辦法讓它自動將任意屬性從一個或兩個set復制到新創建的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

看起來 set 繞過了c 代碼中的__init__ 但是,您將結束Fooset一個實例,它只是沒有機會復制該字段。

除了覆蓋返回新集合的方法之外,我不確定在這種情況下您可以做太多事情。 Set 顯然是為一定的速度而構建的,因此在 c 中做了很多工作。

我試圖回答閱讀它的問題:“如何使“集合”運算符的返回值成為集合子類的類型。忽略給定類的詳細信息以及示例是否一開始就壞了。如果我的閱讀正確,我是從我自己的問題來到這里的,這將是重復的。

這個答案與其他一些答案不同,如下所示:

  • 給定的類(子類)僅通過添加裝飾器來更改
  • 因此足夠通用而不關心給定類的細節(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 所考慮的好處,或者它“免費”為您提供的“某些方法”是什么。 此解決方案旨在使用基類來完成工作並返回子類的實例。 使用成員對象來完成工作的解決方案可能會以類似的方式生成。

假設其他答案是正確的,並且覆蓋所有方法是做到這一點的唯一方法,這是我嘗試以一種適度優雅的方式來做到這一點。 如果添加更多的實例變量,只需更改一段代碼。 不幸的是,如果將新的二元運算符添加到 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')

對我來說,在 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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM