Каков правильный (или лучший) способ подкласса класса множеств Python, добавив новую переменную экземпляра? - PullRequest
11 голосов
/ 28 апреля 2009

Я реализую объект, который почти идентичен набору, но требует дополнительной переменной экземпляра, поэтому я создаю подкласс для встроенного объекта множества. Каков наилучший способ убедиться, что значение этой переменной копируется при копировании одного из моих объектов?

Используя старый модуль множеств, следующий код работал отлично:

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

)

Ответы [ 7 ]

17 голосов
/ 30 апреля 2009

Мой любимый способ упаковки встроенной коллекции:

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. Также легко вставить метакласс, если вы хотите сделать то же самое со списками и диктовками.

6 голосов
/ 14 июля 2011

Я думаю, что рекомендуемый способ сделать это - не создавать подклассы непосредственно из встроенного set, а использовать Абстрактный базовый класс Set, доступный в коллекции .

Использование набора ABC дает вам несколько бесплатных методов в качестве дополнения, поэтому вы можете иметь минимальный класс Set, определяя только __contains__(), __len__() и __iter__(). Если вам нужны некоторые из более хороших методов, таких как 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)
4 голосов
/ 07 сентября 2012

К сожалению, set не следует правилам и __new__ не вызывается для создания новых set объектов, даже если они сохраняют тип. Это явно ошибка в Python (проблема № 1721812, которая не будет исправлена ​​в последовательности 2.x). Вы никогда не сможете получить объект типа X без вызова объекта type, который создает объекты X! Если set.__or__ не собирается вызывать __new__, формально обязано возвращать set объекты вместо объектов подкласса.

Но на самом деле, отмечая пост nosklo выше, ваше оригинальное поведение не имеет никакого смысла. Оператор Set.__or__ не должен повторно использовать ни один из исходных объектов для создания своего результата, он должен вызывать новый, в этом случае его foo должно быть "default"!

Таким образом, практически каждый, кто делает это , должен перегружать эти операторы, чтобы они знали, какая копия foo используется. Если он не зависит от объединяемых элементов, вы можете сделать его классом по умолчанию, и в этом случае он будет принят, поскольку новый объект думает, что он принадлежит к типу подкласса.

Я имею в виду, что ваш пример сработал бы, если бы вы сделали это:

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'
2 голосов
/ 28 апреля 2009

Похоже, установленный обход __init__ в c коде . Однако вы завершите экземпляр Fooset, у него просто не будет возможности скопировать поле.

Помимо переопределения методов, которые возвращают новые наборы, я не уверен, что вы можете сделать слишком много в этом случае. Набор четко построен для определенной скорости, поэтому много работает в c.

2 голосов
/ 28 апреля 2009

set1 | set2 - это операция, которая не изменяет ни один из существующих set, но вместо этого возвращает новый set. Новый set создан и возвращен. Невозможно заставить его автоматически копировать произвольные атрибуты из одного или обоих из set s во вновь созданный 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
0 голосов
/ 28 апреля 2009

Предполагая, что другие ответы верны, и переопределение всех методов - единственный способ сделать это, вот моя попытка в меру элегантного способа сделать это. Если добавлено больше переменных экземпляра, нужно изменить только один фрагмент кода. К сожалению, если новый бинарный оператор будет добавлен к объекту 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')
0 голосов
/ 28 апреля 2009

Для меня это прекрасно работает с использованием Python 2.5.2 на Win32. Используя ваше определение класса и следующий тест:

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
...