¿Cuál es la forma correcta (o mejor) de subclasificar la clase de conjunto de Python, agregando una nueva variable de instancia?

Estoy implementando un objeto que es casi idéntico a un conjunto, pero requiere una variable de instancia adicional, así que estoy subclasificando el objeto de conjunto incorporado. ¿Cuál es la mejor manera de asegurarse de que el valor de esta variable se copia cuando se copia uno de mis objetos?

Usando el módulo de conjuntos antiguos, el siguiente código funcionó perfectamente:

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

pero esto no funciona utilizando el módulo de conjunto incorporado.

La única solución que puedo ver es anular todos los métodos que devuelven un objeto de conjunto copiado … en cuyo caso es mejor que no me moleste en crear una subclase del objeto de conjunto. Seguramente hay una forma estándar de hacer esto?

(Para aclarar, el siguiente código no funciona (la aserción falla):

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

)

Mi forma favorita de envolver los métodos de una colección incorporada:

 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__', ]) 

Esencialmente lo mismo que estás haciendo en tu propia respuesta, pero con menos loc. También es fácil poner una metaclase si quieres hacer lo mismo con listas y dados también.

Creo que la forma recomendada de hacer esto no es hacer una subclase directamente del set incorporado, sino más bien hacer uso del Set clases base abstracto disponible en las colecciones .

El uso de ABC Set le brinda algunos métodos gratuitos como combinación para que pueda tener una clase de Set mínima definiendo solo __contains__() , __len__() y __iter__() . Si desea algunos de los mejores métodos de conjuntos como intersection() y difference() , es probable que tenga que envolverlos.

Aquí está mi bash (este es un frozenset, pero puedes heredar de MutableSet para obtener una versión mutable):

 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) 

Lamentablemente, set no sigue las reglas y __new__ no se llama para crear nuevos set objetos, aunque mantengan el tipo. Esto es claramente un error en Python (problema # 1721812, que no se solucionará en la secuencia 2.x). ¡Nunca debería poder obtener un objeto de tipo X sin llamar al objeto de type que crea objetos X! Si set.__or__ no llamará __new__ , está obligado formalmente a devolver objetos set lugar de objetos subclase.

Pero en realidad, observando la publicación de nosklo arriba, su comportamiento original no tiene ningún sentido. El operador Set.__or__ no debe reutilizar ninguno de los objetos de origen para construir su resultado, debe estar preparando uno nuevo, en cuyo caso su foo debería ser "default" .

Entonces, prácticamente, cualquiera que haga esto debería tener que sobrecargar a esos operadores para que sepan qué copia de foo se usa. Si no depende de la combinación de Foosets, puede hacer que sea una clase predeterminada, en cuyo caso se respetará, porque el nuevo objeto cree que es del tipo de subclase.

Lo que quiero decir es que tu ejemplo funcionaría, más o menos, si hicieras esto:

 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 es una operación que no modificará ninguno de los set existentes, sino que devolverá un nuevo set . El nuevo set es creado y devuelto. No hay forma de hacer que copie automáticamente los atributos arbritarios de uno o ambos set al nuevo set creado, sin personalizar el | __or__ usted mismo definiendo el método __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 

Huellas dactilares:

 MySet(['a', 'c', 'b']) MySet(['c', 'e', 'd']) MySet(['a', 'c', 'b', 'e', 'd']) bar baz bar|baz 

Parece que el conjunto pasa __init__ alto a __init__ en el código c . Sin embargo, finalizará una instancia de Fooset , simplemente no habrá tenido la oportunidad de copiar el campo.

Aparte de anular los métodos que devuelven conjuntos nuevos, no estoy seguro de que pueda hacer demasiado en este caso. El conjunto está claramente construido para una cierta cantidad de velocidad, así que hace mucho trabajo en c.

Suponiendo que las otras respuestas sean correctas y que la única forma de hacer esto es sobrepasar todos los métodos, aquí está mi bash por una forma moderadamente elegante de hacerlo. Si se agregan más variables de instancia, solo una pieza de código necesita cambiar. Desafortunadamente, si se agrega un nuevo operador binario al objeto establecido, este código se romperá, pero no creo que haya una manera de evitar eso. Comentarios bienvenidos!

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

Para mí esto funciona perfectamente utilizando Python 2.5.2 en Win32. Usando tu definición de clase y la siguiente prueba:

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

Obtengo esta salida, que es lo que espero:

 Fooset([1, 2, 4]) default Fooset([1, 2, 4]) bar Fooset([1, 2, 4, 5, 6, 7]) bar