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
- C'est pas la taille qui compte
- Objets indexables
- Les slices
- Module collections
- TP : Une liste chaînée en Python
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' |
-
<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 nouveauChainMap
à partir de l’actuel, ajoutant un dictionnaire à gauche ; - L’attribut
parents
pour obtenir les « parents » de l’actuel, c’est-à-dire leChainMap
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.
-
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, etNone
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 pointerself.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 duslice
; - Implémenter la méthode
__delitem__
pour gérer la suppression de maillons, attention toutefois pour la gestion desslice
, 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.
- La définition du terme séquence : https://docs.python.org/3/glossary.html#term-sequence
- Celle du mapping : https://docs.python.org/3/glossary.html#term-mapping
- Types de séquences : https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range
- Types standards : https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy
- Émuler les conteneurs : https://docs.python.org/3/reference/datamodel.html#emulating-container-types
- Module
collections
: https://docs.python.org/3/library/collections.html