web-dev-qa-db-fra.com

Laisser une classe se comporter comme une liste dans 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(). La variable li doit se comporter comme 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

C'est correct pour une utilisation comme itérateur comme [e for e in li], Mais je n'ai pas le comportement de liste complète comme li.expand(...).

Une première supposition consiste à hériter list en MyFancyList. Mais est-ce la façon recommandée de faire Pythonic? Si oui, que faut-il considérer? Si non, quelle serait une meilleure approche?

40
Michael

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 pour le comportement que vous désirez. Ces méthodes doivent déléguer le travail à la liste réelle à laquelle toute instance de votre classe contient une référence, par exemple:

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

L'implémentation de __getitem__ Seule vous donnera une quantité surprenante de fonctionnalités, par exemple l'itération et le découpage.

>>> 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 que le comportement complet d'une liste, héritez de collections.UserList . UserList est une implémentation complète Python du type de données de liste.

Alors pourquoi ne pas hériter directement de list?

Un problème majeur lié à l'héritage direct de list (ou de tout autre module intégré écrit en C) est que le code des modules intégrés peut ou non appeler des méthodes spéciales remplacées dans des classes définies par l'utilisateur. Voici un extrait pertinent de la dyp pocs :

Officiellement, CPython n'a pas de règle du tout quand la méthode exactement remplacée des sous-classes de types intégrés est appelée ou non implicitement. À titre d'approximation, ces méthodes ne sont jamais appelées par d'autres méthodes intégrées du même objet. Par exemple, un __getitem__ Substitué dans une sous-classe de dict ne sera pas appelé par ex. la méthode intégrée get.

Une autre citation, de Luciano Ramalho's Fluent Python , page 351:

Le sous-classement des types intégrés tels que dict ou list ou str est directement sujet aux erreurs car les méthodes intégrées ignorent généralement les remplacements définis par l'utilisateur. Au lieu de sous-classer les éléments intégrés, dérivez vos classes de UserDict, UserList et UserString à partir du module de collections, qui sont conçues pour être facilement étendues.

... et plus, page 370+:

Comportements incorrects: bug ou fonctionnalité? Les types intégrés dict, list et str sont les éléments constitutifs essentiels de Python lui-même, donc ils doivent être rapides - tout problème de performances en eux aurait un impact important sur tout le reste. C'est pourquoi CPython a adopté les raccourcis qui provoquent un mauvais comportement de leurs méthodes intégrées en ne coopérant pas avec des méthodes remplacées par des sous-classes.

Après avoir joué un peu, les problèmes avec le module intégré list semblent être moins critiques (j'ai essayé de le casser dans Python 3.4 pendant un certain temps mais je n'ai pas trouvé de problème vraiment évident) comportement inattendu), mais je voulais quand même publier une démonstration de ce qui peut arriver en principe, alors 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, la méthode __init__ De dict a ignoré la méthode __setitem__ Remplacée, tandis que la méthode __init__ De notre UserDict ne l'a pas fait. .

62
timgeb

La solution la plus simple consiste à hériter de la classe list:

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

Vous pouvez ensuite utiliser le type MyFancyList comme liste et utiliser ses méthodes spécifiques.

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


EDIT: comme indiqué par @acdr, certaines méthodes renvoyant une copie de liste doivent être remplacées afin de renvoyer un MyFancyList au lieu d'un list.

Un moyen simple de mettre en œuvre cela:

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

Si vous ne voulez pas redéfinir chaque méthode de list, je vous suggère l'approche suivante:

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

Cela ferait fonctionner des méthodes telles que append, extend et ainsi de suite. Attention cependant aux méthodes magiques (par exemple __len__, __getitem__ etc.) ne fonctionnera 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 souhaitez remplacer une méthode de list (extend, par exemple), vous pouvez simplement déclarer la vôtre afin que l'appel ne passe pas par le __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
4
Simone Bronzini

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 un supplément état dans vos listes . Si tel est le cas, vous feriez mieux de simplement déclarer ces fonctions libres et d'ignorer complètement les sous-types; cela évite complètement les problèmes comme list vs UserList, les cas Edge fragiles comme les types de retour pour __add__, 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.

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

3
gntskn