2009-04-28 1 views
8

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

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

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

, но это не работает, используя встроенный в установленном модуле.

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

(Чтобы уточнить, делает следующий код не работы (утверждение не):

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

)

ответ

14

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

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

+0

Это полезный вклад, спасибо. это не похоже на то, что вы набираете много, делая _wrap_methods метод класса, а не функцию, - это только для модульности, которую он дает? – rog

+0

Это действительно круто, спасибо за проницательный вклад! – bjd2385

2

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

Это то, что я подозревал. В этом случае мне придется переопределить __and__, __or__, __rand__, __ror__, __rsub__, __rxor__, __sub__, __xor__, добавить, скопировать, разность, пересечение, симметричное_различие и объединение. Я что-то пропустил? Честно говоря, я искал что-то с простой общностью решения 2.5, которое я перечислил выше ... но отрицательный ответ тоже хорош. Кажется, это немного похоже на ошибку. – rog

-2

Для меня это отлично работает, используя 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 
+0

Да, это работает с 2.5.2, но можете ли вы заставить его работать со встроенным типом набора в python 2.6? – rog

+0

, так как у вас есть 'import sets' в вашем коде и не упоминается 2.6, я предположил, что вы будете использовать модуль sets.py. Если это больше не доступно в версии 2.6, вы, вероятно, не повезете – Ber

2

Это выглядит как набор минует в c code__init__. Однако вы закончите экземпляр Fooset, у него просто не будет возможности скопировать поле.

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

+0

* вздох *. Благодарю. это тоже мое чтение кода на C, но я новичок в python, поэтому подумал, что стоит спросить. я забыл о моих причинах неприличного подкласса в целом - «внешний» подкласс становится зависимым от неопубликованных внутренних деталей реализации его суперкласса. – rog

0

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

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

У вас есть бесконечная рекурсия в методе копирования. x = self.copy() должен быть x = super (Fooset, self) .copy() –

+0

да, вы правы. использует super() лучше, чем явно упоминание суперкласса? – rog

4

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

Использование ABC Set дает вам несколько методов в качестве микширования, поэтому вы можете иметь минимальный класс Set, определяя только __contains__(), __len__() и __iter__(). Если вам нужны некоторые из более удобных методов, таких как intersection() и difference(), вам, вероятно, придется их обернуть.

Вот моя попытка (это один случается быть frozenset-как, но вы можете наследовать от 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) 
3

К сожалению, набор не следовать правилам и __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' 
Смежные вопросы