2009-04-28 6 views
8

J'implémente un objet qui est presque identique à un ensemble, mais qui nécessite une variable d'instance supplémentaire, donc je sous-classe l'objet set intégré. Quelle est la meilleure façon de s'assurer que la valeur de cette variable est copiée quand l'un de mes objets est copié?Quelle est la manière correcte (ou la meilleure) de sous-classer la classe d'ensemble Python, en ajoutant une nouvelle variable d'instance?

En utilisant le module vieux ensembles, le code suivant a parfaitement fonctionné:

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

mais cela ne fonctionne pas à l'aide du module intégré dans le jeu.

La seule solution que je peux voir est de surcharger chaque méthode qui retourne un objet set copié ... dans ce cas, je pourrais aussi bien ne pas déranger l'objet set. Sûrement il y a une manière standard de faire ceci?

(Pour clarifier, le code suivant ne pas travail (l'assertion échoue):

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

)

Répondre

14

Ma façon préférée pour envelopper les méthodes d'une collection intégrée:

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

essentiellement la même chose que vous faites dans votre propre réponse, mais avec moins loc.Il est également facile de mettre une métaclasse si vous voulez faire la même chose avec des listes et des dicts.

+0

c'est une contribution utile, merci. il ne semble pas que vous gagniez beaucoup en faisant de _wrap_methods une méthode de classe plutôt qu'une fonction - est-ce uniquement pour la modularité qu'il donne? – rog

+0

C'est vraiment cool, merci pour la contribution perspicace! – bjd2385

2

set1 | set2 est une opération qui ne modifiera pas non plus existant set, mais un retour nouveau set à la place. Le nouveau set est créé et renvoyé. Il est impossible de copier automatiquement les attributs d'arborescence de l'un ou des deux set sur le set nouvellement créé, sans personnaliser l'opérateur | par 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 

Prints:

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

C'est ce que je soupçonnais. Dans ce cas, je devrai redéfinir __and__, __ou__, __grand__, __ret__, __rsub__, __rx ou __, __sub__, __xor__, ajouter, copier, différence, intersection, symétrie_différence et union. En ai-je manqué? Pour être honnête, je cherchais quelque chose avec la généralité de la solution 2.5 ci-dessus ... mais une réponse négative est bonne aussi. Cela me semble un peu un bug. – rog

-2

Pour moi, cela fonctionne parfaitement en utilisant Python sur Win32. 2.5.2 Utilisez-vous la définition de la classe et le test suivant:

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

Je reçois cette sortie, qui est ce que je pense:

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

oui, cela fonctionne avec 2.5.2, mais pouvez-vous le faire fonctionner avec le type de set intégré dans python 2.6? – rog

+0

puisque vous aviez 'import sets' dans votre code, et je n'ai pas mentionné 2.6, j'ai supposé que vous utiliseriez le module sets.py. Si ce n'est plus disponible en 2.6, vous avez probablement de la chance – Ber

2

Il ressemble ensemble court-circuite __init__ dans le c code. Cependant vous finirez une instance de Fooset, il n'aura tout simplement pas eu la chance de copier le champ. En plus de surcharger les méthodes qui retournent de nouveaux ensembles, je ne suis pas sûr que vous puissiez en faire trop dans ce cas. Set est clairement construit pour une certaine quantité de vitesse, donc beaucoup de travail dans c.

+0

* sigh *. Merci. c'était ma lecture du code C aussi, mais je suis un python newbie alors je pensais que ça valait la peine de demander. J'avais oublié mes raisons de ne pas aimer le sous-classement en général - la sous-classe «externe» devient dépendante des détails de mise en œuvre internes non publiées de sa superclasse. – rog

0

En supposant que les autres réponses sont correctes, et en remplaçant toutes les méthodes est la seule façon de faire cela, voici ma tentative d'une manière modérément élégante de le faire. Si plus de variables d'instance sont ajoutées, un seul morceau de code doit changer. Malheureusement, si un nouvel opérateur binaire est ajouté à l'objet set, ce code va casser, mais je ne pense pas qu'il existe un moyen d'éviter cela. Commentaires bienvenus!

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

Vous avez une récursion infinie dans la méthode de copie. x = self.copy() devrait être x = super (Fooset, self) .copy() –

+0

oui, vous avez raison. utilise super() mieux que de mentionner explicitement la superclasse? – rog

4

Je pense que la méthode recommandée pour ce faire est de ne pas sous-classer directement à partir du set intégré, mais plutôt d'utiliser le Abstract Base Class Set disponible en collections. L'utilisation de l'ABC Set vous donne quelques méthodes gratuitement comme un mix-in de sorte que vous pouvez avoir une classe Set minimale en définissant seulement __contains__(), __len__() et __iter__(). Si vous voulez quelques-unes des plus belles méthodes comme intersection() et difference(), vous devez probablement les emballer.

Voici ma tentative (celui-ci se trouve être un frozenset semblable, mais vous pouvez hériter de MutableSet pour obtenir une version 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) 
3

Malheureusement, ensemble ne respecte pas les règles et __new__ n'est pas appelé pour créer de nouveaux objets set, même s'ils gardent le type. C'est clairement un bug dans Python (problème # 1721812, qui ne sera pas corrigé dans la séquence 2.x). Vous ne devriez jamais pouvoir obtenir un objet de type X sans appeler l'objet type qui crée des objets X! Si set.__or__ ne va pas appeler __new__ il est formellement obligé de retourner set objets au lieu d'objets de sous-classe.

Mais en fait, en notant le poste par nosklo ci-dessus, votre comportement original n'a aucun sens. L'opérateur Set.__or__ ne devrait pas réutiliser l'un ou l'autre des objets source pour construire son résultat, il devrait en créer un nouveau, dans ce cas foo devrait être "default"! Donc, en pratique, toute personne qui fait cela devrait surcharger ces opérateurs pour qu'ils sachent quelle copie de foo est utilisée. Si elle ne dépend pas de la combinaison des Foosets, vous pouvez en faire une classe par défaut, auquel cas elle sera honorée, parce que le nouvel objet pense qu'elle est du type de la sous-classe.

Ce que je veux dire est, votre exemple fonctionnerait, en quelque sorte, si vous avez fait ceci:

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' 
Questions connexes