44 votes

Faire en sorte qu'une classe se comporte comme une liste en Python

J'ai une classe qui est essentiellement une collection/liste de choses. Mais je veux ajouter quelques fonctions supplémentaires à cette liste. Ce que je voudrais, c'est ce qui suit :

  • J'ai une instance li = MyFancyList() . Variable li doit se comporter comme s'il s'agissait d'une liste chaque fois que je l'utilise comme une liste : [e for e in li] , li.expand(...) , for e in li .
  • De plus, il devrait avoir des fonctions spéciales comme li.fancyPrint() , li.getAMetric() , li.getName() .

J'utilise actuellement l'approche suivante :

class MyFancyList:
  def __iter__(self): 
    return self.li 
  def fancyFunc(self):
    # do something fancy

Il est possible de l'utiliser comme itérateur, comme par exemple [e for e in li] mais je n'ai pas le comportement complet de la liste, par exemple li.expand(...) .

Une première idée est d'hériter list en MyFancyList . Mais est-ce la façon pythonique de faire recommandée ? Si oui, que faut-il prendre en compte ? Si non, quelle serait une meilleure approche ?

65voto

timgeb Points 5966

Si vous ne voulez qu'une partie du comportement de la liste, utilisez la composition (c'est-à-dire que vos instances contiennent une référence à une liste réelle) et implémentez uniquement les méthodes nécessaires au comportement que vous souhaitez. Ces méthodes doivent déléguer le travail à la liste réelle à laquelle toute instance de votre classe fait référence, par exemple :

def __getitem__(self, item):
    return self.li[item] # delegate to li.__getitem__

Mise en œuvre de __getitem__ seul vous donnera une quantité surprenante de fonctionnalités, par exemple l'itération et le découpage en tranches.

>>> class WrappedList:
...     def __init__(self, lst):
...         self._lst = lst
...     def __getitem__(self, item):
...         return self._lst[item]
... 
>>> w = WrappedList([1, 2, 3])
>>> for x in w:
...     x
... 
1
2
3
>>> w[1:]
[2, 3]

Si vous voulez le complet d'une liste, héritent de collections.UserList . UserList est une implémentation Python complète du type de données liste.

Alors pourquoi ne pas hériter de list directement ?

Un problème majeur avec l'héritage direct de list (ou tout autre builtin écrit en C) est que le code des builtins peut ou non appeler des méthodes spéciales surchargées dans les classes définies par l'utilisateur. Voici un extrait pertinent du document documentation sur pypy :

Officiellement, CPython n'a aucune règle pour savoir quand exactement les méthodes surchargées des sous-classes de types intégrés sont appelées implicitement ou non. De manière approximative, ces méthodes ne sont jamais appelées par d'autres méthodes intégrées du même objet. Par exemple, une méthode surchargée __getitem__ dans une sous-classe de dict ne sera pas appelée, par exemple, par la fonction intégrée get méthode.

Une autre citation, tirée de l'ouvrage de Luciano Ramalho Python courant page 351 :

Sous-classer directement des types intégrés comme dict, list ou str est source d'erreurs d'erreurs car les méthodes intégrées ignorent la plupart du temps les définies par l'utilisateur. Au lieu de sous-classer les types intégrés, dérivez vos classes de UserDict , UserList et UserString à partir du module collections qui sont conçus pour être facilement étendus.

... et plus encore, page 370+ :

Les modules intégrés qui se comportent mal : bogue ou fonctionnalité ? Les types intégrés dict, list et str sont des éléments essentiels de Python, ils doivent donc être rapides. ils doivent être rapides - tout problème de performance dans ces types aurait un impact sévère sur presque tout le reste. tout le reste. C'est la raison pour laquelle CPython a adopté les raccourcis qui font que leurs méthodes intégrées se comportent mal. se comportent mal en ne coopérant pas avec les méthodes surchargées par les sous-classes.

Après avoir joué un peu, les problèmes liés à l'option list semble être moins critique (j'ai essayé de le casser dans Python 3.4 pendant un certain temps mais je n'ai pas trouvé de comportement inattendu vraiment évident), mais je voulais quand même poster une démonstration de ce qui peut arriver en principe, donc en voici une avec un dict et un UserDict :

>>> class MyDict(dict):
...     def __setitem__(self, key, value):
...         super().__setitem__(key, [value])
... 
>>> d = MyDict(a=1)
>>> d
{'a': 1}

>>> class MyUserDict(UserDict):
...     def __setitem__(self, key, value):
...         super().__setitem__(key, [value])
... 
>>> m = MyUserDict(a=1)
>>> m
{'a': [1]}

Comme vous pouvez le voir, le __init__ méthode de dict a ignoré la surcharge __setitem__ tandis que la méthode __init__ de notre UserDict ne l'a pas fait.

5voto

aluriak Points 2289

La solution la plus simple ici est d'hériter de list classe :

class MyFancyList(list):
    def fancyFunc(self):
        # do something fancy

Vous pouvez alors utiliser MyFancyList comme une liste, et utiliser ses méthodes spécifiques.

L'héritage introduit un couplage fort entre votre objet et les list . L'approche que vous mettez en œuvre est essentiellement un objet proxy. La façon de l'utiliser dépend fortement de la façon dont vous utiliserez l'objet. S'il doit être une liste, alors l'héritage est probablement un bon choix.


EDIT : comme indiqué par @acdr, certaines méthodes retournant une copie de liste devraient être surchargées afin de retourner une copie de liste. MyFancyList au lieu d'un list .

Un moyen simple de mettre cela en œuvre :

class MyFancyList(list):
    def fancyFunc(self):
        # do something fancy
    def __add__(self, *args, **kwargs):
        return MyFancyList(super().__add__(*args, **kwargs))

4voto

Simone Bronzini Points 592

Si vous ne voulez pas redéfinir chaque méthode de list Je vous propose l'approche suivante :

class MyList:
  def __init__(self, list_):
    self.li = list_
  def __getattr__(self, method):
    return getattr(self.li, method)

Ainsi, des méthodes comme append , extend et ainsi de suite, fonctionnent en dehors de la boîte. Attention, toutefois, les méthodes magiques (par ex. __len__ , __getitem__ etc.) ne fonctionneront pas dans ce cas, vous devez donc au moins les redéclarer comme ceci :

class MyList:
  def __init__(self, list_):
    self.li = list_
  def __getattr__(self, method):
    return getattr(self.li, method)
  def __len__(self):
    return len(self.li)
  def __getitem__(self, item):
    return self.li[item]
  def fancyPrint(self):
    # do whatever you want...

Veuillez noter que dans ce cas, si vous voulez surcharger une méthode de l'option list ( extend par exemple), vous pouvez simplement déclarer le vôtre de sorte que l'appel ne passe pas par l'application __getattr__ méthode. Par exemple :

class MyList:
  def __init__(self, list_):
    self.li = list_
  def __getattr__(self, method):
    return getattr(self.li, method)
  def __len__(self):
    return len(self.li)
  def __getitem__(self, item):
    return self.li[item]
  def fancyPrint(self):
    # do whatever you want...
  def extend(self, list_):
    # your own version of extend

3voto

gntskn Points 199

Sur la base des deux exemples de méthodes que vous avez inclus dans votre message ( fancyPrint , findAMetric ), il ne semble pas que vous ayez besoin de stocker des données supplémentaires. état dans vos listes. Si c'est le cas, il est préférable de les déclarer simplement comme des fonctions libres et d'ignorer complètement le sous-typage ; cela évite complètement les problèmes tels que list vs UserList les cas limites fragiles comme les types de retour pour les __add__ des problèmes inattendus de Liskov, etc. Au lieu de cela, vous pouvez écrire vos fonctions, écrire vos tests unitaires pour leur sortie, et être assuré que tout fonctionnera exactement comme prévu.

En outre, cela signifie que vos fonctions fonctionneront avec tout des types itérables (tels que les expressions de générateur) sans effort supplémentaire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X