2009-04-28 14 views
8

Sto implementando un oggetto che è quasi identico a un set, ma richiede una variabile di istanza aggiuntiva, quindi sto creando una sottoclasse dell'oggetto set integrato. Qual è il modo migliore per assicurarsi che il valore di questa variabile venga copiato quando uno dei miei oggetti viene copiato?Qual è il modo corretto (o migliore) di sottoclasse della classe set Python, aggiungendo una nuova variabile di istanza?

Utilizzando il modulo di vecchi set, il seguente codice ha funzionato perfettamente:

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

ma questo non funziona utilizzando il modulo incorporato set.

L'unica soluzione che riesco a vedere è quella di sovrascrivere ogni singolo metodo che restituisce un oggetto set copiato ... nel qual caso potrei anche non dare fastidio alla sottoclasse dell'oggetto set. Sicuramente esiste un modo standard per farlo?

(Per chiarire, il seguente codice fa non lavoro (l'asserzione fallisce):

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

)

risposta

14

Il mio modo preferito per avvolgere i metodi di una collezione built-in:

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

essenzialmente la stessa cosa si sta facendo nella vostra risposta, ma con meno loc.È anche facile inserire un metaclasse se vuoi fare la stessa cosa anche con elenchi e dict.

+0

questo è un contributo utile, grazie. non sembra che tu stia guadagnando molto rendendo _wrap_methods un metodo di classe piuttosto che una funzione - è puramente per la modularità che dà? – rog

+0

Questo è davvero fantastico, grazie per il contributo perspicace! – bjd2385

2

set1 | set2 è un'operazione che non modificherà né esistente set, ma un ritorno nuovo set invece. Il nuovo set viene creato e restituito. Non c'è modo di farlo copiare automaticamente gli attributi arbritari da uno o entrambi i numeri set allo set appena creato, senza personalizzare l'operatore | da solo tramite 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 

Stampe:

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

Questo è quello che sospettavo. In questo caso, dovrò sovrascrivere __and__, __or__, __rand__, __ror__, __rsub__, __rxor__, __sub__, __xor__, add, copy, difference, intersezione, symmetric_difference e union. Ho perso qualcuno? Per essere onesti, stavo cercando qualcosa con la semplice generalità della soluzione 2.5 che ho elencato sopra ... ma anche una risposta negativa è buona. Mi sembra un po 'come un insetto. – rog

-2

Per me questo funziona perfettamente con Python 2.5.2 su Win32. Utilizzando voi definizione di classe e il seguente test:

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

ottengo questa uscita, che è quello che mi aspetto:

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

sì, funziona con 2.5.2, ma puoi farlo funzionare con il set di caratteri incorporato in python 2.6? – rog

+0

dato che nel tuo codice sono presenti "import sets" e non ho menzionato 2.6, ho pensato che avresti usato il modulo sets.py. Se questo non è più disponibile in 2.6, la tua probabile sfortuna è – Ber

2

Sembra set bypassa __init__ nel c code. Comunque finirai un'istanza di Fooset, semplicemente non avresti avuto la possibilità di copiare il campo.

Oltre a ignorare i metodi che restituiscono nuovi set, non sono sicuro che in questo caso si possa fare troppo. Set è chiaramente costruito per una certa quantità di velocità, quindi un sacco di lavoro in c.

+0

* sigh *. Grazie. questa era la mia lettura del codice C, ma sono un novizio pitone quindi ho pensato che valesse la pena di chiedermelo. avevo dimenticato le mie ragioni per cui non amavo le sottoclassi in generale - la sottoclasse "esterna" diventa dipendente dai dettagli di implementazione interna non pubblicati della sua superclasse. – rog

0

Supponendo che le altre risposte siano corrette, e ignorando tutti i metodi è l'unico modo per farlo, ecco il mio tentativo di un modo moderatamente elegante di farlo. Se vengono aggiunte più variabili di istanza, è necessario modificare solo un pezzo di codice. Sfortunatamente se un nuovo operatore binario viene aggiunto all'oggetto set, questo codice si romperà, ma non penso che ci sia un modo per evitarlo. Commenti benvenuto!

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

Hai qualche ricorsione infinita nel metodo di copia. x = self.copy() dovrebbe essere x = super (Fooset, self) .copy() –

+0

sì, hai ragione. sta usando super() meglio di menzionare esplicitamente la superclasse? – rog

4

Penso che il modo consigliato per farlo non sia quello di sottoclasse direttamente dal set integrato, ma piuttosto di utilizzare lo Abstract Base Class Set disponibile in collections.

L'utilizzo del set ABC offre alcuni metodi gratuiti come un mix-in in modo da avere una classe Set minima, definendo solo __contains__(), __len__() e __iter__(). Se vuoi alcuni dei più bei metodi impostati come intersection() e difference(), probabilmente devi avvolgerli.

Ecco il mio tentativo (questo sembra essere un frozenset-like, ma si può ereditare da MutableSet per ottenere una versione mutabile):

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

Purtroppo, insieme non segue le regole e __new__ non è chiamato per creare nuovi oggetti set, anche se mantengono il tipo. Questo è chiaramente un bug in Python (problema n. 1721812, che non verrà corretto nella sequenza 2.x). Non si dovrebbe mai essere in grado di ottenere un oggetto di tipo X senza chiamare l'oggetto type che crea oggetti X! Se non chiama , è formalmente obbligato a restituire gli oggetti set anziché gli oggetti di sottoclasse.

Ma in realtà, notando il post di nosklo sopra, il tuo comportamento originale non ha alcun senso. L'operatore Set.__or__ non deve riutilizzare nessuno degli oggetti sorgente per costruirne il risultato, ma dovrebbe sostituirlo con uno nuovo, nel qual caso il suo foo dovrebbe essere "default"!

Così, praticamente, chiunque stia facendo questo deve sovraccaricare quegli operatori in modo che possano sapere quale copia di foo viene utilizzata. Se non dipende dal fatto che i Fooset siano combinati, puoi renderlo un default di classe, nel qual caso verrà onorato, perché il nuovo oggetto pensa che sia del tipo sottoclasse.

Quello che voglio dire è che il vostro esempio avrebbe funzionato, una sorta di, se avete fatto questo:

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'