Conteneurs

En Python, on appelle conteneur (container) un objet ayant vocation à en contenir d’autres, comme les chaînes de caractères, les listes, les ensembles ou les dictionnaires.

Il existe plusieurs catégories de conteneurs, notamment celle des subscriptables. Ce nom barbare regroupe tous les objets sur lesquels l’opérateur [] peut être utilisé. L’ensemble des types cités dans le premier paragraphe sont subscriptables, à l’exception de l’ensemble (set), qui n’implémente pas l’opération [].

Les subscriptables se divisent en deux nouvelles catégories non exclusives : les indexables et les sliceables. Les premiers sont ceux pouvant être indexés avec des nombres entiers, les seconds pouvant l’être avec des slice (voir plus loin).

On parle plus généralement de séquence quand un conteneur est indexable et sliceable.

Une autre catégorie importante de conteneurs est formée par les mappings : il s’agit des objets qui associent des valeurs à des clefs, comme le font les dictionnaires.

Une séquence et un mapping se caractérisent aussi par le fait qu’ils possèdent une taille, comme nous le verrons par la suite dans ce chapitre.

Les conteneurs, c'est in

Comme indiqué, les conteneurs sont donc des objets qui contiennent d’autres objets. Ils se caractérisent par l’opérateur in : (0, 1, 2, 3) étant un conteneur, il est possible de tester s’il contient telle ou telle valeur à l’aide de cet opérateur.

1
2
3
4
>>> 3 in (0, 1, 2, 3)
True
>>> 4 in (0, 1, 2, 3)
False

Comment cela fonctionne ? C’est très simple. Comme pour de nombreux comportements, Python se base sur des méthodes spéciales des objets. Vous en connaissez déjà probablement, ce sont les méthodes dont les noms débutent et s’achèvent par __.

Ici, l’opérateur in fait simplement appel à la méthode __contains__ de l’objet, qui prend en paramètre l’opérande gauche, et retourne un booléen.

1
2
3
4
>>> 'o' in 'toto'
True
>>> 'toto'.__contains__('o')
True

Il nous suffit ainsi d’implémenter cette méthode pour faire de notre objet un conteneur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> class MyContainer:
...     def __contains__(self, value):
...         return value is not None # contient tout sauf None
...
>>> 'salut' in MyContainer()
True
>>> 1.5 in MyContainer()
True
>>> None in MyContainer()
False

C'est pas la taille qui compte

Un autre point commun partagé par de nombreux conteneurs est qu’ils possèdent une taille. C’est-à-dire qu’ils contiennent un nombre fini et connu d’éléments, et peuvent être passés en paramètre à la fonction len par exemple.

1
2
3
4
5
6
>>> len([1, 2, 3])
3
>>> len(MyContainer())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'MyContainer' has no len()

Comme pour l’opérateur in, la fonction len fait appel à une méthode spéciale de l’objet, __len__, qui ne prend ici aucun paramètre et doit retourner un nombre entier positif.

1
2
3
4
>>> len('toto')
4
>>> 'toto'.__len__()
4

Nous pouvons donc aisément donner une taille à nos objets :

1
2
3
4
5
6
>>> class MySizeable:
...     def __len__(self):
...         return 18
...
>>> len(MySizeable())
18

Je vous invite à faire des essais en retournant d’autres valeurs (nombres négatifs, flottants, chaînes de caractères) pour observer le comportement.

Objets indexables

Nous voilà bien avancés : nous savons mesurer la taille d’un objet, mais pas voir les éléments qu’il contient.

L’accès aux éléments se fait via l’opérateur []. De même que la modification et la suppression, quand celles-ci sont possibles (c’est-à-dire que l’objet est mutable).

1
2
3
4
5
6
7
8
9
>>> numbers = [4, 7, 6]
>>> numbers[2]
6
>>> numbers[1] = 5
>>> numbers
[4, 5, 6]
>>> del numbers[0]
>>> numbers
[5, 6]

Le comportement interne est ici régi par 3 méthodes : __getitem__, __setitem__, et __delitem__.

1
2
3
4
5
6
7
8
9
>>> numbers = [4, 7, 6]
>>> numbers.__getitem__(2)
6
>>> numbers.__setitem__(1, 5)
>>> numbers
[4, 5, 6]
>>> numbers.__delitem__(0)
>>> numbers
[5, 6]

Comme précédemment, nous pouvons donc implémenter ces méthodes dans un nouveau type. Nous allons ici nous contenter de faire un proxy autour d’une liste existante.

Un proxy1 est un objet prévu pour se substituer à un autre, il doit donc répondre aux mêmes méthodes, de façon transparente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class MyList:
    def __init__(self, value=()): # Émulation du constructeur de list
        self.internal = list(value)

    def __len__(self): # Sera utile pour les tests
        return len(self.internal)

    def __getitem__(self, key):
        return self.internal[key] # Équivalent à return self.internal.__getitem__(key)

    def __setitem__(self, key, value):
        self.internal[key] = value

    def __delitem__(self, key):
        del self.internal[key]

Nous pouvons tester notre objet, celui-ci a bien le comportement voulu :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> numbers = MyList('123456')
>>> len(numbers)
6
>>> numbers[1]
'2'
>>> numbers[1] = '0'
>>> numbers[1]
'0'
>>> del numbers[1]
>>> len(numbers)
5
>>> numbers[1]
'3'

  1. <https://fr.wikipedia.org/wiki/Proxy_(patron_de_conception)> 

Les slices

Les « slices » se traduisent en français par « tranches ». Cela signifie que l’on va prendre notre objet et le découper en morceaux. Par exemple, récupérer la première moitié d’une liste, ou cette même liste en ne conservant qu’un élément sur deux.

Les slices sont une syntaxe particulière pour l’indexation, à l’aide du caractère : lors des appels à [].

1
2
3
4
5
6
7
>>> letters = ('a', 'b', 'c', 'd', 'e', 'f')
>>> letters[0:4]
('a', 'b', 'c', 'd')
>>> letters[1:-2]
('b', 'c', 'd')
>>> letters[::2]
('a', 'c', 'e')

Je pense que vous êtes déjà familier avec cette syntaxe. Le slice peut prendre jusqu’à 3 nombres :

  • Le premier est l’indice de départ (0 si omis) ;
  • Le second est l’indice de fin (fin de la liste si omis), l’élément correspondant à cet indice étant exclu ;
  • Le dernier est le pas, le nombre d’éléments passés à chaque itération (1 par défaut) ;

Notons que les slices peuvent aussi servir pour la modification et la suppression :

1
2
3
4
5
6
7
>>> letters = ['a', 'b', 'c', 'd', 'e', 'f']
>>> letters[::2] = 'x', 'y', 'z'
>>> letters
['x', 'b', 'y', 'd', 'z', 'f']
>>> del letters[0:3]
>>> letters
['d', 'z', 'f']

Une bonne chose pour nous est que, même avec les slices, ce sont les 3 mêmes méthodes __getitem__, __setitem__ et __delitem__ qui sont appelées lors des accès. Cela signifie que la classe MyList que nous venons d’implémenter est déjà compatible avec les slices.

En fait, c’est simplement que le paramètre key passé ne représente pas un nombre, mais est un objet de type slice :

1
2
3
4
5
>>> s = slice(1, -1)
>>> 'abcdef'[s]
'bcde'
>>> 'abcdef'[slice(None, None, 2)]
'ace'

Comme vous le voyez, le slice se construit toujours de la même manière, avec 3 nombres pouvant être omis, ou précisés à None pour prendre leur valeur par défaut.

L’objet ainsi construit contient 3 attributs : start, stop, et step.

1
2
3
4
5
6
7
>>> s = slice(1, 2, 3)
>>> s.start
1
>>> s.stop
2
>>> s.step
3

Je vous conseille ce tutoriel de pascal.ortiz pour en savoir plus sur les slices : https://zestedesavoir.com/tutoriels/582/les-slices-en-python/

Module collections

Avant d’en terminer avec les conteneurs, je voulais vous présenter le module collections. Ce module, parfois méconnu, comprend différents conteneurs utiles de la bibliothèque standard, que je vais essayer de vous présenter brièvement.

namedtuple

Les tuples nommés (ou named tuples), sont très semblables aux tuples, dont ils héritent. Ils ajoutent simplement la possibilité de référencer les champs du tuple par un nom plutôt que par un index, pour gagner en lisibilité.

On instancie namedtuple pour créer un nouveau type de tuples nommés. namedtuple prend en paramètre le nom du type et la liste des noms de champs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> from collections import namedtuple
>>> Point2D = namedtuple('Point2D', ['x', 'y'])
>>> p = Point2D(3, 5)
>>> p.x
3
>>> p.y
5
>>> p[0]
3
>>> p
Point2D(x=3, y=5)
>>> Point2D(x=1, y=2)
Point2D(x=1, y=2)

deque

Les queues à deux extrémités (ou double-ended queues contracté en deques), sont des objets proches des listes de Python, mais avec une structure interne différente.

Plutôt qu’avoir un tableau d’éléments, les éléments sont vus comme des maillons liés les uns aux autres.

L’intérêt des deques par rapport aux listes est d’offrir de meilleures performances pour l’insertion/suppression d’éléments en tête et queue de liste, mais moins bonnes pour l’accès à un élément en milieu de liste. Elles peuvent donc être indiquées pour gérer des piles ou des files.

Pour bénéficier de ces optimisations, les deques sont pourvues de méthodes appendleft, extendleft et popleft qui travaillent sur l’extrêmité gauche de la séquence, en plus des habituelles append/extend/pop qui travaillent sur celle de droite.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
>>> from collections import deque
>>> d = deque([1, 2, 3])
>>> d
deque([1, 2, 3])
>>> d.append(4)
>>> d.appendleft(0)
>>> d
deque([0, 1, 2, 3, 4])
>>> d.popleft()
0
>>> d.popleft()
1
>>> d.extendleft([1, 0])
>>> d
deque([0, 1, 2, 3, 4])
>>> d[0], d[1], d[-1]
(0, 1, 4)
>>> d[-1] = 5
>>> d
deque([0, 1, 2, 3, 5])

Les deques ont aussi la possibilité d’être limitées en taille, en supprimant les éléments les plus à droite lors d’une insertion à gauche et inversement. Cela permet la réalisation de tampons circulaires.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>>> d = deque([], 2)
>>> d
deque([], maxlen=2)
>>> d.append(1)
>>> d.append(2)
>>> d
deque([1, 2], maxlen=2)
>>> d.append(3)
>>> d
deque([2, 3], maxlen=2)
>>> d.appendleft(1)
>>> d
deque([1, 2], maxlen=2)
>>> d.maxlen
2

ChainMap

Les ChainMap sont des structures de données permettant de grouper (chaîner) plusieurs dictionnaires (ou mappings) sans les fusionner, ce qui leur permet de se tenir à jour. Elles se comportent comme des dictionnaires et s’occupent de rechercher les éléments dans les mappings qui lui ont été donnés à la construction. Lors de l’insertion de nouveaux éléments, ceux-ci sont ajoutés au premier mapping.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
>>> from collections import ChainMap
>>> d1 = {'a': 0, 'b': 1}
>>> d2 = {'b': 2, 'c': 3}
>>> d3 = {'d': 4}
>>> c = ChainMap(d1, d2, d3)
>>> c
ChainMap({'b': 1, 'a': 0}, {'c': 3, 'b': 2}, {'d': 4})
>>> c['a'], c['b'], c['c'], c['d']
(0, 1, 3, 4)
>>> c['e'] = 5
>>> c
ChainMap({'e': 5, 'b': 1, 'a': 0}, {'c': 3, 'b': 2}, {'d': 4})
>>> d1
{'e': 5, 'b': 1, 'a': 0}
>>> d2['f'] = 6
>>> c['f']
6

Les ChainMap ajoutent quelques fonctionnalités pratiques :

  • L’attribut maps pour obtenir la liste des mappings ;
  • La méthode new_child pour créer un nouveau ChainMap à partir de l’actuel, ajoutant un dictionnaire à gauche ;
  • L’attribut parents pour obtenir les « parents » de l’actuel, c’est-à-dire le ChainMap composé des mêmes mappings excepté le plus à gauche.
1
2
3
4
5
6
7
8
>>> c.maps
[{'e': 5, 'b': 1, 'a': 0}, {'c': 3, 'b': 2, 'f': 6}, {'d': 4}]
>>> c.new_child()
ChainMap({}, {'e': 5, 'b': 1, 'a': 0}, {'c': 3, 'b': 2, 'f': 6}, {'d': 4})
>>> c.new_child({'a': 7})
ChainMap({'a': 7}, {'e': 5, 'b': 1, 'a': 0}, {'c': 3, 'b': 2, 'f': 6}, {'d': 4})
>>> c.parents
ChainMap({'c': 3, 'b': 2, 'f': 6}, {'d': 4})

Les ChainMap se révèlent alors très utiles pour gérer des contextes / espaces de noms imbriqués, comme présenté dans la documentation.

Counter

Les compteurs (Counter) sont des dictionnaires un peu spéciaux qui servent à compter les éléments.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>>> from collections import Counter
>>> c = Counter([1, 2, 3, 1, 3, 1, 5])
>>> c
Counter({1: 3, 3: 2, 2: 1, 5: 1})
>>> c[3]
2
>>> c[4]
0
>>> c[5] += 1
>>> c
Counter({1: 3, 3: 2, 5: 2, 2: 1})
>>> c + Counter({1: 2, 2: 3})
Counter({1: 5, 2: 4, 3: 2, 5: 2})
>>> c - Counter({1: 2, 2: 3})
Counter({3: 2, 5: 2, 1: 1})

On retrouve quelques méthodes utiles pour manipuler les compteurs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> list(c.elements())
[1, 1, 1, 2, 3, 3, 5, 5]
>>> c.most_common()
[(1, 3), (3, 2), (5, 2), (2, 1)]
>>> c.most_common(2)
[(1, 3), (3, 2)]
>>> c.update({5: 1})
>>> c
Counter({1: 3, 5: 3, 3: 2, 2: 1})
>>> c.subtract({2: 4})
>>> c
Counter({1: 3, 5: 3, 3: 2, 2: -3})
>>> +c # éléments positifs
Counter({1: 3, 5: 3, 3: 2})
>>> -c # éléments négatifs
Counter({2: 3})

OrderedDict

Les dictionnaires ordonnées (OrderedDict) sont comme leur nom l’indique des dictionnaires où l’ordre d’insertion des éléments est conservé, et qui sont itérés dans cet ordre.

Certaines implémentations (pypy, CPython 3.6) disposent de dictionaires ordonnés par défaut, mais il est préférable de passer par OrderedDict pour s’en assurer.

Depuis Python 3.61, on peut construire un dictionnaire ordonné de la manière suivante :

1
2
>>> from collections import OrderedDict
>>> d = OrderedDict(b=0, a=1, c=2)

Dans les versions précédentes du langage, l’ordre des paramètres nommés n’étant pas assuré, il faut plutôt procéder avec un conteneur ordonné en paramètre.

1
>>> d = OrderedDict([('b', 0), ('c', 2), ('a', 1)])

Le dictionnaire ordonné est sinon semblable à tout autre dictionnaire.

1
2
3
4
5
6
7
8
9
>>> d['d'] = 4
>>> d['c'] = 5
>>> for key, value in d.items():
...     print(key, value)
...
b 0
c 5
a 1
d 4

defaultdict

Voyons maintenant un dernier type de dictionnaires, les defaultdict (dictionnaires à valeurs par défaut). Les compteurs décrits plus haut sont un exemple de dictionnaires à valeurs par défaut : quand un élément n’existe pas, c’est 0 qui est retourné.

Les defaultdict sont plus génériques que cela. Lors de leur construction, ils prennent en premier paramètre une fonction qui servira à initialiser les éléments manquants.

1
2
3
4
5
6
7
8
>>> from collections import defaultdict
>>> d = defaultdict(lambda: 'x')
>>> d[0] = 'a'
>>> d[1] = 'b'
>>> d[2]
'x'
>>> d
defaultdict(<function <lambda> at 0x7ffa55be42f0>, {0: 'a', 1: 'b', 2: 'x'})

Wrappers

Enfin, 3 classes (UserDict, UserList et UserString) sont présentes dans ce module. Elles permettent par héritage de créer vos propres types de dictionnaires, listes ou chaînes de caractères, pour leur ajouter des méthodes par exemple. Elles gardent une référence vers l’objet qu’elles étendent dans leur attribut data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>>> from collections import UserList
>>> class MyList(UserList):
...     def head(self):
...         return self.data[0]
...     def queue(self):
...         return self.data[1:]
...
>>> l = MyList([1, 2, 3])
>>> l.append(4)
>>> l
[1, 2, 3, 4]
>>> l.head()
1
>>> l.queue()
[2, 3, 4]

Ces classes datent cependant d’un temps que les moins de 20 ans ne peuvent pas connaître où il était impossible d’hériter des types dict, list ou str. Elles ont depuis perdu de leur intérêt.


  1. Je vous invite à consulter cet article sur la sortie de Python 3.6 pour en savoir plus sur les nouveautés apportées par cette version : <https://zestedesavoir.com/articles/1540/sortie-de-python-3-6/> 

TP : Une liste chaînée en Python

Présentation

Nous avons, dans les paragraphes précédents, créé un proxy autour d’une liste pour découvrir le fonctionnement des méthodes décrites.

Dans ce TP, pas à pas, nous créerons notre propre type de liste, à savoir une liste chaînée, c’est-à-dire composée de maillons reliés entre eux. Très courantes dans des langages bas-niveau tels que le C, elles le sont beaucoup moins en Python.

La différence avec le type deque du module collections est que nos listes seront simplement chaînées : un maillon n’aura connaissance que du maillon suivant, pas du précédent.

En plus de nos méthodes d’accès aux éléments, nous implémenterons les méthodes insert et append afin d’ajouter facilement des éléments à notre liste.

Bases

Nous appellerons donc notre classe Deque et, à la manière de list, le constructeur pourra prendre un objet pour pré-remplir notre liste.

Notre liste sera composée de maillons, et nous aurons donc une seconde classe, très simple, pour représenter un maillon : Node. un maillon possède une valeur (value), un maillon suivant (next), et… c’est tout. next pouvant être à None si l’on est en fin de liste.

La liste ne gardera qu’une référence vers le premier maillon, et vers le dernier (deux extrémités, double-ended).

Une seule chose à laquelle penser : quand nous instancions notre maillon, nous voulons que celui-ci référence le maillon suivant. Mais aussi, et surtout, que le maillon précédent le référence. Ainsi, le next du maillon précédent devra pointer vers notre nouveau maillon.

1
2
3
4
class Node:
    def __init__(self, value, next=None):
        self.value = value
        self.next = next

Et notre classe Deque, qui contiendra une référence vers le premier et le dernier maillon, tout en itérant sur le potentiel objet passé au constructeur pour initialiser la liste.

1
2
3
4
5
6
class Deque:
    def __init__(self, iterable=()):
        self.first = None # Premier maillon
        self.last = None # Dernier maillon
        for element in iterable:
            self.append(element)

Notre classe Node étant achevée, toutes les méthodes qui seront données par la suite seront à ajouter à la classe Deque.

append et insert

Si vous avez tenté d’instancier notre liste pour le moment (en lui précisant un paramètre), vous avez remarqué que celle-ci levait une erreur : en effet, nous appelons une méthode append qui n’est actuellement pas définie.

C’est par celle-ci que nous allons commencer, car son comportement est très simple : nous créons un nouveau noeud que nous ajoutons à la fin de la liste. Cela peut se résumer en :

  • Créer un Node avec la valeur spécifiée en paramètre, et None comme maillon suivant ;
  • Lier le nouveau maillon à l’actuel dernieer maillon ;
  • Faire pointer la fin de liste sur ce nouveau maillon ;
  • Et, ne pas oublier, dans le cas où notre liste est actuellement vide, de faire pointer le début de liste sur ce maillon.
1
2
3
4
5
6
7
def append(self, value):
    node = Node(value, None)
    if self.last is not None:
        self.last.next = node
    self.last = node
    if self.first is None:
        self.first = node

Vient maintenant la méthode insert, qui permet d’insérer une nouvelle valeur à n’importe quel endroit de la liste. Nous allons pour cela nous aider d’une première méthode get_node pour récupérer un maillon dans la liste (un objet de type Node, donc, pas sa valeur), qui nous servira encore beaucoup par la suite.

Cette méthode prendra un nombre en paramètre, correspondant à l’indice du maillon que nous voulons extraire, et itérera sur les maillons de la liste jusqu’à arriver à celui-ci. Nous nous contenterons pour le moment de gérer les nombres positifs. Nous lèverons de plus une erreur si l’indice ne correspond à aucun maillon.

1
2
3
4
5
6
7
8
def get_node(self, n):
    node = self.first
    while n > 0 and node is not None:
        node = node.next
        n -= 1
    if node is None:
        raise IndexError("list index out of range")
    return node

Notre méthode insert prend deux paramètres : la position et la valeur à insérer. Cette méthode aura trois comportements, suivant que l’on cherche à insérer la valeur en tête de liste, en fin, ou au milieu.

  • Dans le premier cas, il nous faudra créer un nouveau maillon, avec self.first comme maillon suivant, puis faire pointer self.first sur ce nouveau maillon ;
  • Dans les deux autres, il faudra repérer le maillon précédent à l’aide de get_node, puis insérer notre maillon à la suite de celui-ci ;
  • Dans tous les cas, il faudra faire pointer self.last vers notre maillon si celui-ci est en fin de liste.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def insert(self, i, value):
    if not i:
        node = Node(value, next=self.first)
        self.first = node
    else:
        prev = self.get_node(i - 1)
        node = Node(value, prev.next)
    prev.next = node
    if node.next is None:
        self.last = node

__contains__

Pour faire de notre liste un conteneur, il nous faut implémenter l’opération in et donc la méthode __contains__. Celle-ci, à la manière de get_node, itérera sur tous les maillons jusqu’à en trouver un correspondant à la valeur passée en paramètre et ainsi retourner True. Si la valeur n’est pas trouvée après avoir itéré sur toute la liste, il convient alors de retourner False.

1
2
3
4
5
6
7
def __contains__(self, value):
    node = self.first
    while node is not None:
        if node.value == value:
            return True
        node = node.next
    return False

__len__

Nous souhaitons maintenant pouvoir calculer la taille de notre liste. La méthode __len__ sera aussi très similaire aux précédentes, elle itère simplement du début à la fin en comptant le nombre de noeuds.

1
2
3
4
5
6
7
def __len__(self):
    node = self.first
    size = 0
    while node is not None:
        node = node.next
        size += 1
    return size

__getitem__ et __setitem__

Dans un premier temps, implémentons ces deux méthodes sans tenir compte des slice. Une grande part du travail est déjà réalisé par get_node :

1
2
3
4
5
def __getitem__(self, key):
    return self.get_node(key).value

def __setitem__(self, key, value):
    self.get_node(key).value = value

Puis vient la gestion des slice. Rappelons-nous ce que contient un slice, à savoir un indice de début, un indice de fin et un pas. Cela ne vous rappelle pas quelque chose ? Si, c’est exactement ce à quoi correspond range. À ceci près que les valeurs None n’ont aucune signification pour les range.

Mais l’objet slice possède une méthode indices, qui, en lui donnant la taille de notre ensemble, retourne les paramètres à passer à range (en gérant de plus pour nous les indices négatifs).

Nous pouvons ainsi obtenir un range à l’aide de l’expression range(*key.indices(len(self))) (en considérant que key est un objet de type slice). Il ne nous reste donc plus qu’à itérer sur le range, et :

  • Créer une nouvelle liste contenant les éléments extraits dans le cas d’un __getitem__ ;
  • Modifier un à un les éléments dans le cas d’un __setitem__.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def __getitem__(self, key):
    if isinstance(key, slice):
        new_list = Deque()
        indices = range(*key.indices(len(self)))
        for i in indices:
            new_list.append(self[i])
        return new_list
    else:
        return self.get_node(key).value

def __setitem__(self, key, value):
    if isinstance(key, slice):
        indices = range(*key.indices(len(self)))
        for i, v in zip(indices, value):
            self[i] = v
    else:
        self.get_node(key).value = value

`

Si vous ne comprenez pas bien ce que fait la fonction zip, celle-ci assemble deux listes (nous verrons dans le chapitre suivant que c’est un peu plus large que cela), de la manière suivante :

1
2
>>> list(zip([1, 2, 3], ['a', 'b', 'c']))
[(1, 'a'), (2, 'b'), (3, 'c')]

Aller plus loin

Ce TP touche à sa fin, mais pour aller plus loin, voici une liste non exhaustive de fonctionnalités qu’il nous reste à implémenter :

  • Gérer des nombres négatifs pour l’indexation ;
  • Gérer les cas de __setitem__ où la liste de valeurs a une taille différente de la taille du slice ;
  • Implémenter la méthode __delitem__ pour gérer la suppression de maillons, attention toutefois pour la gestion des slice, les indices seront invalidés après chaque suppression ;
  • Implémenter les méthodes spéciales __str__ et __repr__ pour afficher facilement notre liste.

Nous sommes maintenant à la fin du premier chapitre. En guise de conclusion, je vais vous fournir une liste de sources tirées de la documentation officielle permettant d’aller plus loin sur ces sujets.