Mappages Python : un guide complet
L’une des principales structures de données que vous découvrirez au début de votre parcours d’apprentissage Python est le dictionnaire. Les dictionnaires sont les mappages de Python les plus courants et les plus connus. Cependant, il existe d’autres mappages dans la bibliothèque standard de Python et dans les modules tiers. Les mappages partagent des caractéristiques communes, et comprendre ces traits communs vous aidera à les utiliser plus efficacement.
Dans ce didacticiel, vous découvrirez :
- Caractéristiques de base d'un mappage
- Opérations qui sont communes à la plupart des mappages
- Classes de base abstraites
Mapping
etMutableMapping
- Mappages mutables et immuables définis par l'utilisateur et comment les créer
Ce didacticiel suppose que vous êtes familier avec les types de données intégrés de Python, en particulier les dictionnaires, ainsi qu'avec les bases de la programmation orientée objet.
Comprendre les principales caractéristiques des mappages Python
Un mappage est une collection qui vous permet de rechercher une clé et de récupérer sa valeur. Les clés des mappages peuvent être des objets d’un large éventail de types. Cependant, dans la plupart des mappages, certains types d'objets ne peuvent pas être utilisés comme clés, comme vous l'apprendrez plus loin dans ce didacticiel.
Le paragraphe précédent décrivait les mappages comme des collections. Une collection est un conteneur itérable qui a une taille définie. Cependant, les mappages disposent également de fonctionnalités supplémentaires. Vous explorerez chacune de ces caractéristiques de mappage avec des exemples tirés des principaux types de mappage de Python.
La fonctionnalité la plus caractéristique des mappages est la possibilité de récupérer une valeur à l’aide d’une clé. Vous pouvez utiliser un dictionnaire pour démontrer cette opération :
>>> points = {
... "Denise": 3,
... "Igor": 2,
... "Sarah": 3,
... "Trevor": 1,
... }
>>> points["Sarah"]
3
>>> points["Matt"]
Traceback (most recent call last):
...
KeyError: 'Matt'
Le dictionnaire points
contient quatre éléments, chacun avec une clé et une valeur. Vous pouvez utiliser la clé entre crochets pour récupérer la valeur associée à cette clé. Cependant, si la clé n'existe pas dans le dictionnaire, le code génère une KeyError
.
Vous pouvez utiliser l'un des mappages du module collections
de la bibliothèque standard pour attribuer une valeur par défaut aux clés qui ne sont pas présentes dans la collection. Le type defaultdict
inclut un appelable qui est appelé chaque fois que vous essayez d'accéder à une clé qui n'existe pas. Si vous souhaitez que la valeur par défaut soit zéro, vous pouvez utiliser une fonction lambda
qui renvoie 0 comme premier argument dans defaultdict
:
>>> from collections import defaultdict
>>> points_default = defaultdict(
... lambda: 0,
... points,
... )
>>> points_default
defaultdict(<function <lambda> at 0x104a95da0>, {'Denise': 3,
'Igor': 2, 'Sarah': 3, 'Trevor': 1})
>>> points_default["Sarah"]
3
>>> points_default["Matt"]
0
>>> points_default
defaultdict(<function <lambda> at 0x103e6c700>, {'Denise': 3,
'Igor': 2, 'Sarah': 3, 'Trevor': 1, 'Matt': 0})
Le constructeur defaultdict
a deux arguments dans cet exemple. Le premier argument est l’appelable utilisé lorsqu’une valeur par défaut est nécessaire. Le deuxième argument est le dictionnaire que vous avez créé précédemment. Vous pouvez utiliser n'importe quel argument valide lorsque vous appelez dict()
comme deuxième argument dans defaultdict()
ou omettre cet argument pour créer un defaultdict
vide. .
Lorsque vous accédez à une clé manquante dans le dictionnaire, la clé est ajoutée et la valeur par défaut lui est attribuée. Vous pouvez également créer le même objet points_default
en utilisant l'appelable int
comme premier argument puisque l'appel de int()
sans argument renvoie 0.
Tous les mappages sont également des collections, ce qui signifie qu'il s'agit de conteneurs itérables d'une longueur définie. Vous pouvez explorer ces caractéristiques avec un autre mappage dans la bibliothèque standard de Python, collections.Counter
:
>>> from collections import Counter
>>> letters = Counter("learning python")
>>> letters
Counter({'n': 3, 'l': 1, 'e': 1, 'a': 1, 'r': 1, 'i': 1, 'g': 1,
' ': 1, 'p': 1, 'y': 1, 't': 1, 'h': 1, 'o': 1})
Les lettres de la chaîne "learning python"
sont converties en clés dans Counter
, et le nombre d'occurrences de chaque lettre est utilisé comme valeur correspondant à chaque clé.
Vous pouvez confirmer que ce mappage est itérable, a une longueur définie et est un conteneur :
>>> for letter in letters:
... print(letter)
...
l
e
a
r
n
i
g
p
y
t
h
o
>>> len(letters)
13
>>> "n" in letters
True
>>> "x" in letters
False
Vous pouvez utiliser l'objet Counter
letters
dans une boucle for
, ce qui confirme qu'il est itérable. Tous les mappages sont itérables. Cependant, l’itération parcourt les clés et non les valeurs. Vous verrez comment parcourir les valeurs ou les clés et les valeurs plus loin dans ce didacticiel.
La fonction intégrée len()
renvoie le nombre d'éléments dans le mappage. Ceci est égal au nombre de caractères uniques dans la chaîne d'origine, y compris le caractère espace. L'objet est dimensionné puisque len()
renvoie une valeur.
Vous pouvez utiliser le mot-clé in
pour confirmer quels éléments se trouvent dans le mappage. Cette vérification à elle seule ne suffit pas à confirmer que le mappage est un conteneur. Cependant, vous pouvez également accéder directement à la méthode spéciale .__contains__()
de l'objet :
>>> letters.__contains__("n")
True
Comme vous pouvez le voir, la présence de cette méthode spéciale confirme que letters
est un conteneur.
La méthode spéciale .__getitem__()
dans les mappages
Les caractéristiques que vous avez découvertes dans la première section sont définies à l'aide de méthodes spéciales au sein des définitions de classe. Par conséquent, les mappages ont une méthode spéciale .__iter__()
pour les rendre itérables, une méthode spéciale .__contains__()
pour les définir en tant que conteneurs et un .__len__()
méthode spéciale pour leur donner une taille.
Les mappages ont également la méthode spéciale .__getitem__()
pour les rendre inscriptibles. Un objet peut être inscriptible lorsque vous pouvez ajouter des crochets après l'objet, comme my_object[item]
. Dans un mappage, la valeur que vous utilisez entre crochets est la clé dans une paire clé-valeur, et elle est utilisée pour récupérer la valeur qui correspond à la clé.
La méthode spéciale .__getitem__()
fournit l'interface pour la notation entre crochets. Dans le dictionnaire Python et d'autres mappages, cette récupération de données est implémentée à l'aide d'une table de hachage, ce qui rend l'accès aux données efficace. Vous pouvez en savoir plus sur les tables de hachage et sur la manière dont elles sont implémentées dans les dictionnaires Python dans Créer une table de hachage en Python avec TDD.
Si vous créez votre propre mappage, vous devrez implémenter la méthode spéciale .__getitem__()
pour récupérer les valeurs des clés. Dans la plupart des cas, la meilleure option consiste à utiliser le dictionnaire Python ou d’autres mappages implémentés dans la bibliothèque standard de Python pour utiliser l’accès efficace aux données déjà implémenté dans ces structures de données.
Vous explorerez ces idées lorsque vous créerez un mappage défini par l’utilisateur plus loin dans ce didacticiel.
Clés, valeurs et éléments dans les mappages
Revenez à l'un des mappages que vous avez utilisés plus tôt dans ce didacticiel, le dictionnaire points
:
>>> points = {
... "Denise": 3,
... "Igor": 2,
... "Sarah": 3,
... "Trevor": 1,
... }
Le dictionnaire se compose de quatre clés associées à quatre valeurs. Chaque mappage est caractérisé par ces paires clé-valeur. Chaque paire clé-valeur est un élément. Par conséquent, ce dictionnaire comporte quatre éléments. Vous pouvez le confirmer en utilisant len(points)
, qui renvoie l'entier 4.
Les mappages Python ont trois méthodes appelées .keys()
, .values()
et .items()
. Vous pouvez commencer par explorer les deux premiers d’entre eux :
>>> points.keys()
dict_keys(['Denise', 'Igor', 'Sarah', 'Trevor'])
>>> points.values()
dict_values([3, 2, 3, 1])
Ces méthodes sont utiles lorsque vous devez accéder uniquement aux clés ou uniquement aux valeurs d'un dictionnaire. La méthode .items()
renvoie les éléments du mappage appariés en tuples :
>>> points.items()
dict_items([('Denise', 3), ('Igor', 2), ('Sarah', 3), ('Trevor', 1)])
L'objet renvoyé par .items()
est utile lorsque vous devez accéder à la paire clé-valeur en tant qu'itérable, par exemple, si vous devez parcourir le mappage et accéder à la clé et à la valeur pour chacun. article:
>>> for name, number in points.items():
... print(f"Number of points for {name}: {number}")
...
Number of points for Denise: 3
Number of points for Igor: 2
Number of points for Sarah: 3
Number of points for Trevor: 1
Vous pouvez également confirmer que d'autres mappages disposent de ces méthodes :
>>> from collections import Counter
>>> letters = Counter("learning python")
>>> letters
Counter({'n': 3, 'l': 1, 'e': 1, 'a': 1, 'r': 1, 'i': 1, 'g': 1,
' ': 1, 'p': 1, 'y': 1, 't': 1, 'h': 1, 'o': 1})
>>> letters.keys()
dict_keys(['l', 'e', 'a', 'r', 'n', 'i', 'g', ' ', 'p', 'y', 't',
'h', 'o'])
>>> letters.values()
dict_values([1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1])
>>> letters.items()
dict_items([('l', 1), ('e', 1), ('a', 1), ('r', 1), ('n', 3),
('i', 1), ('g', 1), (' ', 1), ('p', 1), ('y', 1), ('t', 1),
('h', 1), ('o', 1)])
Ces méthodes ne renvoient ni liste ni tuple. Au lieu de cela, ils renvoient des objets dict_keys
, dict_values
ou dict_items
. Même les méthodes appelées sur l'objet Counter
renvoient les trois mêmes types de données puisque de nombreux mappages reposent sur l'implémentation dict
.
Les objets dict_keys
, dict_values
et dict_items
sont des vues de dictionnaire. Ces objets ne contiennent pas leurs propres données, mais ils fournissent une vue des données stockées dans le mappage. Pour expérimenter cette idée, vous pouvez attribuer l'une des vues à une variable, puis modifier les données dans le mappage d'origine :
>>> values_in_points = points.values()
>>> values_in_points
dict_values([3, 2, 3, 1])
>>> points["Igor"] += 10
>>> values_in_points
dict_values([3, 12, 3, 1])
Vous attribuez l'objet dict_values
renvoyé par .values()
à values_in_points
. Lorsque vous mettez à jour le dictionnaire, les valeurs dans values_in_points
changent également.
La distinction entre les clés, les valeurs et les éléments est essentielle lorsque vous travaillez avec des mappages. Vous reverrez les méthodes pour y accéder plus tard dans ce didacticiel lorsque vous créerez votre propre mappage.
Comparaison entre les mappages, les séquences et les ensembles
Plus tôt dans ce didacticiel, vous avez appris qu'un mappage est une collection dans laquelle vous pouvez accéder à une valeur à l'aide d'une clé qui lui est associée. Les mappages ne sont pas la seule collection en Python. Les séquences et les ensembles sont également des collections. Les séquences courantes incluent des listes, des tuples et des chaînes.
Il est utile de comprendre les similitudes et les différences entre ces catégories pour mieux comprendre les mappages. Toutes les collections sont des conteneurs itérables qui ont une longueur définie. Les objets appartenant à l’une de ces trois catégories partagent ces caractéristiques.
Les mappages et les séquences sont indicables. Vous pouvez utiliser la notation entre crochets pour accéder aux valeurs à partir des mappages et des séquences. Cette caractéristique est définie par la méthode spéciale .__getitem__()
. Les ensembles, en revanche, ne peuvent pas être indicés :
>>> # Mapping
>>> points = {
... "Denise": 3,
... "Igor": 2,
... "Sarah": 3,
... "Trevor": 1,
... }
>>> points["Igor"]
2
>>> # Sequence
>>> numbers = [4, 10, 34]
>>> numbers[1]
10
>>> # Set
>>> numbers_set = {4, 10, 34}
>>> numbers_set[1]
Traceback (most recent call last):
...
TypeError: 'set' object is not subscriptable
>>> numbers_set.__getitem__
Traceback (most recent call last):
...
AttributeError: 'set' object has no attribute '__getitem__'.
Did you mean: '__getstate__'?
Vous pouvez utiliser la notation entre crochets pour accéder aux valeurs d'un dictionnaire et d'une liste. La même chose s’applique à tous les mappages et séquences. Mais comme les ensembles n'ont pas la méthode spéciale .__getitem__()
, ils ne peuvent pas être indicés.
Cependant, il existe des différences entre les mappages et les séquences lors de l'utilisation de la notation entre crochets. Les séquences sont des structures ordonnées et la notation entre crochets permet l'indexation à l'aide d'entiers qui représentent la position de l'élément dans la séquence. Avec les séquences, vous pouvez également inclure une tranche entre crochets. Seuls les entiers et les tranches sont autorisés lors de l'indice d'une séquence.
Les mappages n'ont pas besoin d'être ordonnés et vous ne pouvez pas utiliser la notation entre crochets pour accéder à un élément en fonction de sa position dans la structure. Au lieu de cela, vous utilisez la clé dans une paire clé-élément entre crochets. De plus, les objets que vous pouvez utiliser entre crochets dans un mappage ne se limitent pas aux entiers et aux tranches.
Cependant, pour la plupart des mappages, vous ne pouvez pas utiliser d’objets mutables ou de structures immuables contenant des objets mutables. Cette exigence est imposée par la table de hachage utilisée pour implémenter les dictionnaires et autres mappages :
>>> {[0, 0]: None, [1, 1]: None}
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'
>>> from collections import Counter
>>> number_groups = ([1, 2, 3], [1, 2, 3], [2, 3, 4])
>>> Counter(number_groups)
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'
Comme le montre cet exemple, vous ne pouvez pas utiliser une liste comme clé dans un dictionnaire car les listes sont modifiables et non hachables. Et même si number_groups
est un tuple, vous ne pouvez pas l'utiliser pour créer un objet Counter
puisque le tuple contient des listes.
Les mappages ne sont pas une structure de données ordonnée. Cependant, les éléments des dictionnaires conservent l’ordre dans lequel ils ont été ajoutés. Cette fonctionnalité est présente depuis Python 3.6 et a été ajoutée à la description du langage formel dans Python 3.7. Même si l’ordre des éléments du dictionnaire est conservé, les dictionnaires ne constituent pas une structure ordonnée comme les séquences. Voici une démonstration de cette différence :
>>> [1, 2] == [2, 1]
False
>>> {"one": 1, "two": 2} == {"two": 2, "one": 1}
True
Les deux listes ne sont pas égales puisque les valeurs sont dans des positions différentes. Cependant, les deux dictionnaires sont égaux car ils ont les mêmes paires clé-valeur, même si l’ordre dans lequel ils sont inclus est différent.
Dans la plupart des mappages basés sur le dictionnaire Python, les clés doivent être uniques. Il s'agit d'une autre exigence de la table de hachage utilisée pour implémenter les dictionnaires et autres mappages. Cependant, les éléments des séquences ne doivent pas nécessairement être uniques. De nombreuses séquences ont des valeurs répétées.
L'obligation d'utiliser des objets hachables uniques comme clés dans les mappages Python vient de l'implémentation de dictionnaires. Ce n’est pas une exigence inhérente aux mappages. Cependant, la plupart des mappages sont construits sur le dictionnaire Python et partagent donc la même exigence.
Les ensembles ont également des valeurs uniques qui doivent être hachables. Les éléments d'ensemble partagent beaucoup de points communs avec les clés de dictionnaire puisqu'ils sont également implémentés à l'aide d'une table de hachage. Cependant, les éléments d’un ensemble n’ont pas de paire clé-valeur et vous ne pouvez pas accéder à un élément d’un ensemble à l’aide de la notation entre crochets.
Explorer les classes de base abstraites Mapping
et MutableMapping
Python possède des classes de base abstraites qui définissent des interfaces pour les catégories de types de données telles que les mappages. Dans cette section, vous découvrirez les classes de base abstraites Mapping
et MutableMapping
, que vous trouverez dans le module collections.abc
.
Remarque : Il existe des alias pour ces classes de base abstraites et d'autres dans le module typing
. Cependant, ceux-ci sont obsolètes depuis Python 3.9 et ne doivent pas être utilisés.
Ces classes peuvent être utilisées pour vérifier qu'un objet est une instance d'un mappage :
>>> from collections import Counter
>>> from collections.abc import Mapping, MutableMapping
>>> points = {
... "Denise": 3,
... "Igor": 2,
... "Sarah": 3,
... "Trevor": 1,
... }
>>> isinstance(points, Mapping)
True
>>> isinstance(points, MutableMapping)
True
>>> letters = Counter("learning python")
>>> isinstance(letters, MutableMapping)
True
Le dictionnaire points
est un Mapping
et un MutableMapping
. Tous les objets MutableMapping
sont également des objets Mapping
. L'objet Counter
renvoie également True
lorsque vous vérifiez s'il s'agit d'un MutableMapping
. Vous pouvez vérifier si points
est un dict
et si letters
est un objet Counter
à la place.
Cependant, si tout ce dont vous avez besoin est qu’un objet soit un mappage, il est préférable d’utiliser les classes de base abstraites. Cette idée correspond bien à la philosophie de typage de canard de Python puisque vous vérifiez ce qu'un objet peut faire plutôt que de quel type il s'agit.
Les classes de base abstraites peuvent également être utilisées pour des indications de type et pour créer des mappages personnalisés via l'héritage. Cependant, lorsque vous devez créer un mappage défini par l'utilisateur, vous disposez également d'autres options, que vous découvrirez plus loin dans ce didacticiel.
Caractéristiques de la classe de base abstraite Mapping
La classe de base abstraite Mapping
définit l'interface pour tous les mappages en fournissant plusieurs méthodes et en garantissant que les méthodes spéciales requises sont incluses.
Les méthodes spéciales obligatoires que vous devez définir lorsque vous créez un mappage sont les suivantes :
.__getitem__()
: définit comment accéder aux valeurs en utilisant la notation entre crochets..__iter__()
: définit comment parcourir le mappage..__len__()
: Définit la taille du mappage.
La classe de base abstraite Mapping
fournit également les méthodes suivantes :
.__contains__
: définit comment déterminer l’appartenance au mappage..__eq__()
: définit comment déterminer l'égalité de deux objets..__ne__()
: définit comment déterminer quand deux objets ne sont pas égaux..keys()
: Définit comment accéder aux clés dans le mappage..values()
: définit comment accéder aux valeurs dans le mappage..items()
: définit comment accéder aux paires clé-valeur dans le mappage..get()
: définit une manière alternative d'accéder aux valeurs à l'aide de clés. Cette méthode vous permet de définir une valeur par défaut à utiliser si la clé n'est pas présente dans le mappage.
Chaque mappage en Python inclut au moins ces méthodes. Dans la section suivante, vous découvrirez les méthodes également incluses dans les mappages mutables.
Caractéristiques de la classe de base abstraite MutableMapping
La classe de base abstraite Mapping
n'inclut aucune méthode nécessaire pour apporter des modifications au mappage. Il crée un mappage immuable. Cependant, il existe une deuxième classe de base abstraite appelée MutableMapping
pour créer la version mutable.
MutableMapping
hérite de Mapping
. Par conséquent, il inclut toutes les méthodes présentes dans Mapping
, mais comporte deux méthodes spéciales supplémentaires requises :
.__setitem__()
: Définit comment définir une nouvelle valeur pour une clé..__delitem__()
: Définit comment supprimer un élément dans le mappage.
La classe de base abstraite MutableMapping
ajoute également ces méthodes :
.pop()
: définit comment supprimer une clé d'un mappage et renvoyer sa valeur..popitem()
: définit comment supprimer et renvoyer l'élément le plus récemment ajouté dans un mappage..clear()
: Définit comment supprimer tous les éléments du mappage..update()
: Définit comment mettre à jour un dictionnaire en utilisant les données passées en argument à cette méthode..setdefault()
: définit comment ajouter une clé avec une valeur par défaut si la clé n'est pas déjà dans le mappage.
Vous connaissez peut-être bon nombre de ces méthodes grâce à l'utilisation de la structure de données dict
de Python. Vous trouverez ces méthodes dans tous les mappages mutables. Les mappages peuvent également avoir d'autres méthodes en plus de cet ensemble. Par exemple, un objet Counter
a une méthode .most_common()
, qui renvoie la clé la plus courante.
Dans le reste de ce didacticiel, vous utiliserez Mapping
et MutableMapping
pour créer un mappage personnalisé et utiliser plusieurs de ces méthodes.
Création d'un mappage défini par l'utilisateur
Dans les sections suivantes de ce didacticiel, vous allez créer une classe personnalisée pour un mappage défini par l'utilisateur. Vous allez créer un cours pour créer des éléments de menu pour une pizzeria locale. Le propriétaire du restaurant a remarqué que de nombreux clients commandent les mauvaises pizzas et se plaignent ensuite. Une partie du problème réside dans le fait que plusieurs éléments du menu utilisent des noms inconnus et que les clients font souvent des erreurs avec les noms de pizza commençant par la même lettre.
Le propriétaire de la pizzeria a décidé de n'inclure que les pizzas commençant par des lettres différentes. Vous allez créer un mappage pour vous assurer que les clés ne commencent pas par la même lettre. Vous devriez également pouvoir accéder à la valeur associée à chaque clé en utilisant la clé complète ou simplement la première lettre. Par conséquent, menu["Margherita"]
et menu["m"]
renverront la même valeur. La valeur liée à chaque touche est le prix de la pizza.
Dans cette section, vous allez créer une classe qui hérite de la classe de base abstraite Mapping
. Cela vous donnera un bon aperçu du fonctionnement des mappages. Dans les sections suivantes, vous travaillerez sur d’autres façons de créer la même classe.
Vous pouvez commencer par créer une nouvelle classe qui hérite de Mapping
et accepte un dictionnaire comme seul argument :
from collections.abc import Mapping
class PizzaMenu(Mapping):
def __init__(self, menu: dict):
self._menu = menu
La classe PizzaMenu
hérite de Mapping
et sa méthode spéciale .__init__()
accepte un dictionnaire, qui est affecté au ._menu
attribut de données. Le trait de soulignement de début dans ._menu
indique que cet attribut n'est pas destiné à être accessible en dehors de la classe.
Vous pouvez tester cette classe dans une session REPL :
>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
Traceback (most recent call last):
...
TypeError: Can't instantiate abstract class PizzaMenu without
an implementation for abstract methods '__getitem__',
'__iter__', '__len__'
Vous tentez de créer une instance de PizzaMenu
à l'aide d'un dictionnaire contenant deux éléments. Mais le code génère une TypeError
. Puisque PizzaMenu
hérite de la classe de base abstraite Mapping
, il doit avoir les trois méthodes spéciales requises .__getitem__()
, .__iter__( )
et .__len__()
.
L'objet PizzaMenu
inclut l'attribut de données ._menu
, qui est un dictionnaire. Par conséquent, vous pouvez utiliser les propriétés de ce dictionnaire pour définir ces méthodes spéciales :
from collections.abc import Mapping
class PizzaMenu(Mapping):
def __init__(self, menu: dict):
self._menu = menu
def __getitem__(self, key):
return self._menu[key]
def __iter__(self):
return iter(self._menu)
def __len__(self):
return len(self._menu)
Vous définissez .__getitem__()
de sorte que lorsque vous accédez à la valeur d'une clé dans PizzaMenu
, l'objet renvoie la valeur correspondant à cette clé dans le ._menu
dictionnaire. La définition de .__iter__()
garantit que parcourir un objet PizzaMenu
équivaut à parcourir le dictionnaire ._menu
et . __len__()
définit la taille de l'objet PizzaMenu
comme la taille du dictionnaire ._menu
.
Remarque : Vous devrez démarrer une nouvelle session si vous utilisez le REPL standard de Python chaque fois que vous apportez des modifications à la définition de classe dans pizza_menu.py
. Vous ne pouvez pas réécrire l'instruction d'importation dans la même session REPL, car cela n'importera pas la classe mise à jour. Il est également possible d'utiliser importlib
depuis la bibliothèque standard pour recharger un module, mais il est plus simple de démarrer un nouveau REPL.
Alternativement, vous pouvez appuyer sur F6 pour recharger les modules importés si vous utilisez un autre REPL tel que bpython.
Vous pouvez tester la classe dans une nouvelle session REPL :
>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
<pizza_menu.PizzaMenu object at 0x102fb6b10>
Cela fonctionne maintenant. Vous avez créé une instance de PizzaMenu
. Cependant, le résultat lorsque vous affichez l’objet n’est pas utile. C'est une bonne pratique de définir la méthode spéciale .__repr__()
pour une classe définie par l'utilisateur, qui fournit une représentation sous forme de chaîne conviviale pour le programmeur. Vous pouvez également définir la méthode spéciale .__str__()
pour fournir une représentation sous forme de chaîne conviviale :
from collections.abc import Mapping
class PizzaMenu(Mapping):
# ...
def __repr__(self):
return f"{self.__class__.__name__}({self._menu})"
def __str__(self):
return str(self._menu)
La méthode spéciale .__repr__()
produit une sortie qui peut être utilisée pour recréer l'objet. Vous pouvez utiliser le nom de la classe directement dans la chaîne, mais à la place, vous utilisez self.__class__.__name__
pour récupérer le nom de la classe de manière dynamique. Cette version garantit que la méthode .__repr__()
fonctionne également comme prévu pour les sous-classes de PizzaMenu
.
Remarque : Certains programmeurs considèrent qu'il est plus pythonique d'obtenir le nom de la classe en utilisant une expression type(self).__name__
plus concise au lieu d'accéder directement au .__class__
attribut.
Vous pouvez confirmer le résultat de ces méthodes dans une nouvelle session REPL :
>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})
>>> print(menu)
{'Margherita': 9.5, 'Pepperoni': 10.5}
Vous créez une instance de la classe à partir d'un dictionnaire et affichez l'objet. Cliquez ci-dessous pour voir une méthode spéciale alternative .__init__()
pour PizzaMenu
.
La méthode .__init__()
que vous avez créée pour PizzaMenu
accepte un dictionnaire comme argument. Par conséquent, vous ne pouvez créer un PizzaMenu
qu'à partir d'un autre dictionnaire. Vous pouvez modifier la méthode .__init__()
pour accepter les mêmes types d'arguments que vous pouvez utiliser pour créer une instance d'un dictionnaire lors de l'utilisation de dict()
.
Il existe quatre types d'arguments que vous pouvez utiliser lors de la création d'un dictionnaire avec dict()
:
- Aucun argument : Vous créez un dictionnaire vide lorsque vous appelez
dict()
sans argument. - Mappage : Vous pouvez utiliser n'importe quel mappage comme argument dans
dict()
, qui crée un nouveau dictionnaire à partir du mappage. - Itérable : Vous pouvez utiliser un itérable qui a des paires d'objets comme argument dans
dict()
. Le premier élément de chaque paire devient la clé et le deuxième élément est sa valeur dans le nouveau dictionnaire. **kwargs
: Vous pouvez utiliser n'importe quel nombre d'arguments de mots-clés lors de l'appel dedict()
. Les mots-clés deviennent des clés de dictionnaire et les valeurs d'argument deviennent des valeurs de dictionnaire.
Vous pouvez reproduire cette approche flexible directement dans PizzaMenu
:
from collections.abc import Mapping
class PizzaMenu(Mapping):
def __init__(self, menu=None, /, **kwargs):
self._menu = dict(menu or {}, **kwargs)
# ...
Cette version vous permet de créer un objet PizzaMenu
de différentes manières. Si des arguments de mot-clé sont présents, vous appelez dict()
avec soit menu
, soit un dictionnaire vide comme premier argument. Le mot-clé or
utilise une évaluation de court-circuit afin que menu
soit utilisé s'il est véridique et le dictionnaire vide si menu
est faux. Si aucun argument mot-clé n'est présent, vous appelez dict()
soit avec le premier argument, soit avec le dictionnaire vide si menu
est manquant :
>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})
>>> menu = PizzaMenu(Margherita=9.5, Pepperoni=10.5)
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})
>>> menu = PizzaMenu([("Margherita", 9.5), ("Pepperoni", 10.5)])
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})
>>> menu = PizzaMenu()
>>> menu
PizzaMenu({})
Vous initialisez une instance de PizzaMenu
à l'aide d'un dictionnaire, d'arguments de mots-clés, d'une liste de tuples et enfin, sans arguments.
Vous utiliserez la méthode spéciale .__init__()
plus simple dans le reste de ce didacticiel pour vous permettre de vous concentrer sur d'autres aspects du mappage.
Votre prochaine étape consiste à personnaliser cette classe pour répondre à l'exigence selon laquelle aucune clé ne commence par la même lettre.
Empêcher les noms de pizza de commencer par la même lettre
Le propriétaire de la pizzeria ne veut pas que les noms de pizza commencent par la même lettre. Vous choisissez de déclencher une exception si vous essayez de créer une instance de PizzaMenu
avec des noms non valides :
from collections.abc import Mapping
class PizzaMenu(Mapping):
def __init__(self, menu: dict):
self._menu = {}
first_letters = set()
for key, value in menu.items():
first_letter = key[0].lower()
if first_letter in first_letters:
raise ValueError(
f"'{key}' is invalid."
" All pizzas must have unique first letters"
)
first_letters.add(first_letter)
self._menu[key] = value
# ...
Vous créez un ensemble appelé first_letters
dans .__init__()
. Lorsque vous parcourez le dictionnaire, vous convertissez la première lettre en minuscule et vérifiez si la lettre est déjà dans l'ensemble first_letters
. Puisqu'aucune première lettre ne peut être répétée, le code dans la boucle génère une erreur s'il trouve une lettre répétée.
Si le code ne génère pas d’erreur, vous ajoutez la première lettre à l’ensemble pour vous assurer qu’il n’y a pas de noms invalides plus tard dans l’itération. Vous ajoutez également la valeur au dictionnaire ._menu
, que vous initialisez comme dictionnaire vide au début de la méthode .__init__()
.
Vous pouvez vérifier ce comportement dans une nouvelle session REPL. Vous créez un dictionnaire de noms proposés à utiliser comme argument pour PizzaMenu()
:
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
... "Margherita": 9.50,
... "Pepperoni": 10.50,
... "Hawaiian": 11.50,
... "Meat Feast": 12.50,
... "Capricciosa": 12.50,
... "Napoletana": 11.50,
... "Pizza Bianca": 10.50,
... }
>>> menu = PizzaMenu(proposed_pizzas)
Traceback (most recent call last):
...
ValueError: 'Meat Feast' is invalid. All pizzas must have
unique first letters
Les noms dans proposed_pizzas
contiennent des entrées non valides. Il y a deux pizzas qui commencent par M et deux qui commencent par P. Vous pouvez renommer les pizzas et réessayer :
>>> proposed_pizzas = {
... "Margherita": 9.50,
... "Pepperoni": 10.50,
... "Hawaiian": 11.50,
... "Feast of Meat": 12.50,
... "Capricciosa": 12.50,
... "Napoletana": 11.50,
... "Bianca": 10.50,
... }
>>> menu = PizzaMenu(proposed_pizzas)
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5, 'Hawaiian': 11.5,
'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
'Bianca': 10.5})
Maintenant qu'il n'y a plus de premières lettres répétées dans les noms de pizza, vous pouvez créer une instance de PizzaMenu
.
Si vous êtes offensé par l’inclusion d’une pizza hawaïenne, continuez à lire. Si l’ananas sur une pizza vous convient, vous pouvez sauter cette section !
Vous pouvez vous assurer qu'aucune pizza hawaïenne ne figure dans votre menu en ajoutant .__init__()
:
from collections.abc import Mapping
class PizzaMenu(Mapping):
def __init__(self, menu: dict):
self._menu = {}
first_letters = set()
for key, value in menu.items():
if key.lower() in ("hawaiian", "pineapple"):
raise ValueError(
"What?! Hawaiian pizza is not allowed"
)
first_letter = key[0].lower()
if first_letter in first_letters:
raise ValueError(
f"'{key}' is invalid."
" All pizzas must have unique first letters"
)
first_letters.add(first_letter)
self._menu[key] = value
# ...
Vous ajoutez une condition supplémentaire lorsque vous parcourez les clés du dictionnaire pour exclure la pizza hawaïenne. Vous interdisez également la pizza à l’ananas pour faire bonne mesure :
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
... "Margherita": 9.50,
... "Pepperoni": 10.50,
... "Hawaiian": 11.50,
... "Feast of Meat": 12.50,
... "Capricciosa": 12.50,
... "Napoletana": 11.50,
... "Bianca": 10.50,
... }
>>> menu = PizzaMenu(proposed_pizzas)
Traceback (most recent call last):
...
ValueError: What?! Hawaiian pizza is not allowed
Les pizzas hawaïennes sont désormais interdites.
Dans la section suivante, vous ajouterez plus de fonctionnalités à PizzaMenu
pour vous permettre d'accéder à une valeur en utilisant soit le nom complet de la pizza, soit simplement sa première lettre.
Ajouter un autre moyen d'accéder aux valeurs dans PizzaMenu
Puisque toutes les pizzas ont des premières lettres uniques, vous pouvez modifier la classe afin de pouvoir utiliser la première lettre pour accéder à une valeur de PizzaMenu
. Par exemple, supposons que vous souhaitiez pouvoir utiliser menu["Margherita"]
ou menu["m"]
pour accéder au prix d'une pizza Margherita.
Vous pouvez ajouter chaque première lettre comme clé dans ._menu
et lui attribuer la même valeur que la clé avec le nom complet de la pizza. Cependant, cela duplique les données. Vous devez également faire attention lorsque vous modifiez le prix d’une pizza pour vous assurer de modifier la valeur associée à la clé à une seule lettre.
Au lieu de cela, vous pouvez créer un dictionnaire qui mappe la première lettre au nom de la pizza commençant par cette lettre. Vous rassemblez déjà les premières lettres de chaque pizza dans un ensemble pour vous assurer qu’il n’y a pas de répétitions. Vous pouvez refactoriser first_letter
pour en faire un dictionnaire au lieu d'un ensemble. Les clés du dictionnaire sont également uniques, elles peuvent donc être utilisées à la place d'un ensemble :
from collections.abc import Mapping
class PizzaMenu(Mapping):
def __init__(self, menu: dict):
self._menu = {}
self._first_letters = {}
for key, value in menu.items():
first_letter = key[0].lower()
if first_letter in self._first_letters:
raise ValueError(
f"'{key}' is invalid."
" All pizzas must have unique first letters"
)
self._first_letters[first_letter] = key
self._menu[key] = value
# ...
Vous remplacez l'ensemble par un dictionnaire et vous le définissez comme attribut de données de l'instance puisque vous devrez utiliser ce dictionnaire ailleurs dans la définition de la classe. Vous pouvez toujours vérifier si la première lettre du nom d'une pizza figure déjà dans le dictionnaire. Cependant, vous pouvez désormais également lier le nom complet de la pizza en l’ajoutant comme valeur.
Vous devez également modifier .__getitem__()
pour activer l'utilisation d'une seule lettre entre crochets lorsque vous accédez à une valeur à partir d'un objet PizzaMenu
:
from collections.abc import Mapping
class PizzaMenu(Mapping):
# ...
def __getitem__(self, key):
if key not in self._menu and len(key) > 1:
raise KeyError(key)
key = self._first_letters.get(key[0].lower(), key)
return self._menu[key]
# ...
La méthode spéciale .__getitem__()
peut désormais accepter un argument d'une seule lettre. Si l'argument affecté au paramètre key
n'est pas dans ._menu
et n'est pas un seul caractère, alors vous déclenchez une exception puisque la clé n'est pas valide.
Ensuite, vous appelez .get()
sur self._first_letters
, qui est un dictionnaire. Vous incluez le paramètre key
comme valeur par défaut dans cet appel. Si key
est une seule lettre présente dans ._first_letters
, .get()
renvoie sa valeur dans ce dictionnaire. Cette valeur est réaffectée à key
. Cependant, si l'argument de .__getitem__()
n'est pas un élément de ._first_letters
, le paramètre key
est inchangé puisque c'est la valeur par défaut dans .get()
.
Vous pouvez confirmer ce changement dans une nouvelle session REPL :
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
... "Margherita": 9.50,
... "Pepperoni": 10.50,
... "Hawaiian": 11.50,
... "Feast of Meat": 12.50,
... "Capricciosa": 12.50,
... "Napoletana": 11.50,
... "Bianca": 10.50,
... }
>>> menu = PizzaMenu(proposed_pizzas)
>>> menu["Margherita"]
9.5
>>> menu["m"]
9.5
La classe est désormais plus flexible. Vous pouvez accéder au prix d'une pizza en utilisant son nom complet ou simplement la première lettre.
Dans la section suivante, vous explorerez d’autres méthodes que vous attendez d’un mappage.
Remplacer les autres méthodes requises pour PizzaMenu
Vous avez découvert les caractéristiques communes à tous les mappages plus tôt dans ce didacticiel. Puisque PizzaMenu
est une sous-classe de Mapping
, il hérite des méthodes dont disposent tous les mappages.
Vous pouvez vérifier qu'un objet PizzaMenu
se comporte comme prévu lorsque vous effectuez des opérations courantes :
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
... "Margherita": 9.50,
... "Pepperoni": 10.50,
... "Hawaiian": 11.50,
... "Feast of Meat": 12.50,
... "Capricciosa": 12.50,
... "Napoletana": 11.50,
... "Bianca": 10.50,
... }
>>> menu = PizzaMenu(proposed_pizzas)
>>> another_menu = PizzaMenu(proposed_pizzas)
>>> menu is another_menu
False
>>> menu == another_menu
True
>>> for item in menu:
... print(item)
...
Margherita
Pepperoni
Hawaiian
Feast of Meat
Capricciosa
Napoletana
Bianca
>>> "Margherita" in menu
True
>>> "m" in menu
True
Vous créez deux objets PizzaMenu
à partir du même dictionnaire. Les objets sont différents et, par conséquent, le mot-clé is
renvoie False
lors de la comparaison des deux objets. Cependant, l'opérateur d'égalité ==
renvoie True
. Ainsi, les objets sont égaux si tous les éléments de ._menu
sont égaux. Puisque vous n'avez pas défini .__eq__()
, Python utilise .__iter__()
pour parcourir les deux objets et comparer leurs valeurs.
Dans la session REPL, vous confirmez également que l'itération dans un PizzaMenu
parcourt les clés, comme pour les autres mappages.
Enfin, vous confirmez que vous pouvez vérifier si un nom de pizza est membre de l'objet PizzaMenu
à l'aide du mot-clé in
. Puisque vous n'avez pas défini .__contains__()
, Python utilise la méthode spéciale .__getitem__()
pour rechercher le nom de la pizza.
Cependant, cela montre également que la lettre m est membre du menu puisque vous avez modifié .__getitem__()
pour vous assurer que vous pouvez utiliser une seule lettre dans la notation entre crochets. Si vous préférez ne pas inclure de lettres simples en tant que membres de l'objet PizzaMenu
, vous pouvez définir la méthode spéciale .__contains__()
:
from collections.abc import Mapping
class PizzaMenu(Mapping):
# ...
def __contains__(self, key):
return key in self._menu
Lorsque la méthode .__contains__()
est présente, Python l'utilise pour vérifier l'appartenance. Cela contourne .__getitem__()
et vérifie si la clé est membre du dictionnaire stocké dans ._menu
. Vous pouvez confirmer que les lettres simples ne sont plus considérées comme membres de l'objet dans une nouvelle session REPL :
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
... "Margherita": 9.50,
... "Pepperoni": 10.50,
... "Hawaiian": 11.50,
... "Feast of Meat": 12.50,
... "Capricciosa": 12.50,
... "Napoletana": 11.50,
... "Bianca": 10.50,
... }
>>> menu = PizzaMenu(proposed_pizzas)
>>> "Margherita" in menu
True
>>> "m" in menu
False
"Margherita"
est toujours membre de l'objet PizzaMenu
, mais "m"
n'est plus membre.
Vous pouvez également explorer les méthodes qui renvoient les clés, les valeurs et les éléments du mappage :
>>> menu.keys()
KeysView(PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5,
'Hawaiian': 11.5, 'Feast of Meat': 12.5, 'Capricciosa': 12.5,
'Napoletana': 11.5, 'Bianca': 10.5}))
>>> menu.values()
ValuesView(PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5,
'Hawaiian': 11.5, 'Feast of Meat': 12.5, 'Capricciosa': 12.5,
'Napoletana': 11.5, 'Bianca': 10.5}))
>>> menu.items()
ItemsView(PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5,
'Hawaiian': 11.5, 'Feast of Meat': 12.5, 'Capricciosa': 12.5,
'Napoletana': 11.5, 'Bianca': 10.5}))
Les méthodes .keys()
, .values()
et .items()
existent puisqu'elles sont héritées de la classe de base abstraite, mais ils n'affichent pas les valeurs attendues. Au lieu de cela, ils affichent l'objet entier, qui est la représentation sous forme de chaîne renvoyée par .__repr__()
.
Cependant, vous pouvez parcourir ces vues pour récupérer les valeurs correctes :
>>> for key in menu.keys():
... print(key)
...
Margherita
Pepperoni
Hawaiian
Feast of Meat
Capricciosa
Napoletana
Bianca
>>> for value in menu.values():
... print(value)
...
9.5
10.5
11.5
12.5
12.5
11.5
10.5
>>> for item in menu.items():
... print(item)
...
('Margherita', 9.5)
('Pepperoni', 10.5)
('Hawaiian', 11.5)
('Feast of Meat', 12.5)
('Capricciosa', 12.5)
('Napoletana', 11.5)
('Bianca', 10.5)
Ces méthodes fonctionnent comme prévu, mais leurs représentations sous forme de chaîne n'affichent pas les données que vous attendez. Vous pouvez remplacer les méthodes .keys()
, .values()
et .items()
dans le PizzaMenu
définition de classe si vous souhaitez modifier cet affichage, mais ce n'est pas nécessaire.
Aucune des méthodes de la classe de base abstraite Mapping
ne vous permet de modifier le contenu du mappage. Il s'agit d'un mappage immuable. Dans la section suivante, vous allez transformer PizzaMenu
en un mappage mutable.
Création d'un mappage mutable défini par l'utilisateur
Plus tôt dans ce didacticiel, vous avez découvert les méthodes supplémentaires incluses dans l'interface MutableMapping
. Vous pouvez commencer à convertir la classe PizzaMenu
que vous avez créée dans la section précédente en un mappage mutable en héritant de la classe de base abstraite MutableMapping
sans apporter d'autres modifications pour l'instant :
from collections.abc import MutableMapping
class PizzaMenu(MutableMapping):
# ...
Vous pouvez essayer de créer une instance de cette classe dans une nouvelle session REPL :
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
... "Margherita": 9.50,
... "Pepperoni": 10.50,
... "Hawaiian": 11.50,
... "Feast of Meat": 12.50,
... "Capricciosa": 12.50,
... "Napoletana": 11.50,
... "Bianca": 10.50,
... }
>>> menu = PizzaMenu(proposed_pizzas)
Traceback (most recent call last):
...
TypeError: Can't instantiate abstract class PizzaMenu without an
implementation for abstract methods '__delitem__', '__setitem__'
Le mappage immuable que vous avez créé dans la section précédente comportait trois méthodes spéciales requises. Les mappages mutables en ont deux autres : .__delitem__()
et .__setitem__()
. Vous devez donc les inclure lors du sous-classement de MutableMapping
.
Modifier, ajouter et supprimer des éléments du menu Pizza
Vous pouvez commencer par ajouter .__delitem__()
à la définition de classe :
from collections.abc import MutableMapping
class PizzaMenu(MutableMapping):
# ...
def __delitem__(self, key):
if key not in self._menu and len(key) > 1:
raise KeyError(key)
key = self._first_letters.pop(key[0].lower(), key)
del self._menu[key]
La méthode spéciale .__delitem__()
suit un modèle similaire à .__getitem__()
. Si une clé n'est pas une simple lettre et n'est pas membre de ._menu
, la méthode génère une KeyError
. Ensuite, vous appelez .first_letters.pop()
et incluez key
comme valeur par défaut. La méthode .pop()
supprime et renvoie un élément, mais elle renvoie la valeur par défaut si l'élément n'est pas dans le dictionnaire.
Par conséquent, si une clé est une seule lettre contenue dans ._first_letters
, elle sera supprimée de ce dictionnaire. La dernière ligne supprime l'entrée pizza de ._menu
. Cela supprime le nom de la pizza des deux dictionnaires.
La méthode .__setitem__()
nécessite plus de discussion car vous devez considérer plusieurs options :
- Si vous utilisez le nom complet d'une pizza existante lorsque vous définissez une nouvelle valeur,
.__setitem__()
devrait modifier la valeur de l'élément existant dans._menu
. - Si vous utilisez une seule lettre qui correspond à une pizza existante,
.__setitem__()
devrait également modifier la valeur de l'élément existant dans._menu
. - Si vous utilisez un nom de pizza qui n'existe pas déjà dans
._menu
, le code doit vérifier l'unicité de la première lettre avant d'ajouter le nouvel élément à._menu
.
Vous pouvez inclure ces points dans la définition de .__setitem__()
:
from collections.abc import MutableMapping
class PizzaMenu(MutableMapping):
# ...
def __setitem__(self, key, value):
first_letter = key[0].lower()
if len(key) == 1:
key = self._first_letters.get(first_letter, key)
if key in self._menu:
self._menu[key] = value
elif first_letter in self._first_letters:
raise ValueError(
f"'{key}' is invalid."
" All pizzas must have unique first letters"
)
else:
self._first_letters[first_letter] = key
self._menu[key] = value
Cette méthode effectue les actions suivantes :
- Il attribue la première lettre de la clé à
first_letter
. - Si la clé est une seule lettre, il récupère le nom complet de la pizza et le réaffecte à
key
. Cetteclé
est utilisée dans la suite de cette méthode. S'il n'y a pas de pizza correspondante, laclé
reste inchangée pour permettre l'ajout d'une nouvelle pizza avec un nom à une seule lettre. - Si la clé est un nom de pizza complet qui se trouve dans
._menu
, la nouvelle valeur est attribuée à cette clé. - Si la clé n'est pas dans
._menu
mais que sa première lettre est dans._first_letters
, alors ce nom de pizza n'est pas valide puisqu'il commence par une lettre déjà utilisée. La méthode génère uneValueError
. - Enfin, l’option restante concerne une clé nouvelle et valide. La première lettre est ajoutée à
._first_letters
, et le nom et le prix de la pizza sont ajoutés sous forme de paire clé-valeur dans._menu
.
Notez comment vous répétez le code qui déclenche deux fois ValueError
. Vous pouvez éviter cette répétition en ajoutant une nouvelle méthode :
from collections.abc import MutableMapping
class PizzaMenu(MutableMapping):
def __init__(self, menu: dict):
self._menu = {}
self._first_letters = {}
for key, value in menu.items():
first_letter = key[0].lower()
if first_letter in self._first_letters:
self._raise_duplicate_key_error(key)
self._first_letters[first_letter] = key
self._menu[key] = value
def _raise_duplicate_key_error(self, key):
raise ValueError(
f"'{key}' is invalid."
" All pizzas must have unique first letters"
)
# ...
def __setitem__(self, key, value):
first_letter = key[0].lower()
if len(key) == 1:
key = self._first_letters.get(first_letter, key)
if key in self._menu:
self._menu[key] = value
elif first_letter in self._first_letters:
self._raise_duplicate_key_error(key)
else:
self._first_letters[first_letter] = key
self._menu[key] = value
La nouvelle méthode ._raise_duplicate_key_error()
peut être appelée chaque fois qu'il y a un nom invalide. Vous l'utilisez dans .__init__()
et .__setitem__()
.
Vous pouvez maintenant essayer de muter un objet PizzaMenu
dans une nouvelle session REPL :
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
... "Margherita": 9.50,
... "Pepperoni": 10.50,
... "Hawaiian": 11.50,
... "Feast of Meat": 12.50,
... "Capricciosa": 12.50,
... "Napoletana": 11.50,
... "Bianca": 10.50,
... }
>>> menu = PizzaMenu(proposed_pizzas)
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5, 'Hawaiian': 11.5,
'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
'Bianca': 10.5})
>>> menu["m"] = 10.25
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 10.5, 'Hawaiian': 11.5,
'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
'Bianca': 10.5})
>>> menu["Pepperoni"] += 1.25
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75, 'Hawaiian': 11.5,
'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
'Bianca': 10.5})
Vous pouvez désormais modifier la valeur d'un élément en utilisant soit une seule lettre, soit le nom complet lorsque vous y accédez. Vous pouvez également ajouter de nouvelles valeurs au mappage :
>>> menu["Regina"] = 13
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75, 'Hawaiian': 11.5,
'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
'Bianca': 10.5, 'Regina': 13})
Le mappage mutable PizzaMenu
a un nouvel élément, qui est ajouté à la fin puisque les dictionnaires préservent l'ordre d'insertion. Cependant, vous ne pouvez pas ajouter une nouvelle pizza si elle partage la même première lettre qu’une pizza déjà au menu :
>>> menu["Plain Pizza"] = 10
Traceback (most recent call last):
...
ValueError: 'Plain Pizza' is an invalid name. All pizzas must
have unique first letters
Vous pouvez également supprimer des éléments du mappage :
>>> del menu["Hawaiian"]
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75, 'Feast of Meat': 12.5,
'Capricciosa': 12.5, 'Napoletana': 11.5, 'Bianca': 10.5})
>>> menu["h"]
Traceback (most recent call last):
...
KeyError: 'h'
>>> menu["Hawaiian"]
Traceback (most recent call last):
...
KeyError: 'Hawaiian'
Une fois que vous avez supprimé la pizza hawaïenne, vous obtenez une KeyError
lorsque vous essayez d'y accéder, en utilisant soit une seule lettre, soit le nom complet.
Utiliser d'autres méthodes qui modifient les mappages
La classe de base abstraite MutableMapping
ajoute également plus de méthodes à la classe, telles que .pop()
et .update()
. Vous pouvez vérifier si ceux-ci fonctionnent comme prévu :
>>> menu.pop("n")
11.5
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75,
'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Bianca': 10.5})
>>> menu.update({"Margherita": 11.5, "c": 14})
>>> menu
PizzaMenu({'Margherita': 11.5, 'Pepperoni': 11.75,
'Feast of Meat': 12.5, 'Capricciosa': 14, 'Bianca': 10.5})
>>> menu.update({"Festive Pizza": 16})
Traceback (most recent call last):
...
ValueError: 'Festive Pizza' is an invalid name. All pizzas must
have unique first letters
Vous pouvez utiliser une seule lettre dans .pop()
, ce qui supprime la pizza Napoletana. La méthode .update()
fonctionne également avec des noms de pizza complets ou des lettres simples. Le prix de la pizza Capricciosa est mis à jour puisque vous incluez la clé "c"
lorsque vous appelez .update()
.
Vous ne pouvez pas non plus utiliser .update()
pour ajouter un nom de pizza non valide. La pizza festive a été rejetée car il existe déjà un autre nom de pizza commençant par F.
Cela montre que vous n'avez pas besoin de définir toutes ces méthodes, car les méthodes spéciales que vous avez déjà définies peuvent suffire. À titre d'exercice, vous pouvez vérifier que les méthodes ajoutées par MutableMapping
n'ont pas besoin d'être remplacées dans cet exemple.
Voici la version finale de la classe PizzaMenu
qui hérite de MutableMapping
:
from collections.abc import MutableMapping
class PizzaMenu(MutableMapping):
def __init__(self, menu: dict):
self._menu = {}
self._first_letters = {}
for key, value in menu.items():
first_letter = key[0].lower()
if first_letter in self._first_letters:
self._raise_duplicate_key_error(key)
self._first_letters[first_letter] = key
self._menu[key] = value
def _raise_duplicate_key_error(self, key):
raise ValueError(
f"'{key}' is invalid."
" All pizzas must have unique first letters"
)
def __getitem__(self, key):
if key not in self._menu and len(key) > 1:
raise KeyError(key)
key = self._first_letters.get(key[0].lower(), key)
return self._menu[key]
def __setitem__(self, key, value):
first_letter = key[0].lower()
if len(key) == 1:
key = self._first_letters.get(first_letter, key)
if key in self._menu:
self._menu[key] = value
elif first_letter in self._first_letters:
self._raise_duplicate_key_error(key)
else:
self._first_letters[first_letter] = key
self._menu[key] = value
def __delitem__(self, key):
if key not in self._menu and len(key) > 1:
raise KeyError(key)
key = self._first_letters.pop(key[0].lower(), key)
del self._menu[key]
def __iter__(self):
return iter(self._menu)
def __len__(self):
return len(self._menu)
def __repr__(self):
return f"{self.__class__.__name__}({self._menu})"
def __str__(self):
return str(self._menu)
def __contains__(self, key):
return key in self._menu
Cette version contient toutes les méthodes abordées dans cette section du didacticiel.
Vous écrirez une autre version de cette classe dans la section suivante de ce didacticiel.
Héritage de dict
et collections.UserDict
Lorsque vous héritez de Mapping
ou MutableMapping
, vous devez définir toutes les méthodes requises. Le Mapping
nécessite que vous définissiez au moins .__getitem__()
, .__iter__()
et .__len__()
, et MutableMapping
nécessite également .__setitem__()
et .__delitem__()
. Vous avez un contrôle total lors de la définition du mappage.
Dans les sections précédentes, vous avez créé une classe à partir de ces classes de base abstraites. Ceci est utile pour aider à comprendre ce qui se passe dans un mappage.
Cependant, lorsque vous créez un mappage personnalisé, vous souhaitez souvent le modéliser sur un dictionnaire. Il existe d'autres options pour créer des mappages personnalisés. Dans la section suivante, vous allez recréer le mappage personnalisé pour le menu de pizza en héritant directement de dict
.
Une classe qui hérite de dict
Dans les versions précédentes de Python, il n'était pas possible de sous-classer des types intégrés comme dict
. Cependant, ce n’est plus le cas. Néanmoins, il existe des défis lors de l'héritage de dict
.
Vous pouvez commencer à recréer la classe pour hériter de dict
et définir les deux premières méthodes. Toutes les méthodes que vous définirez seront similaires à celles de la section précédente mais présenteront des différences petites et importantes. Vous pouvez distinguer le nom de la classe en appelant la nouvelle classe PizzaMenuDict
:
class PizzaMenuDict(dict):
def __init__(self, menu: dict):
_menu = {}
self._first_letters = {}
for key, value in menu.items():
first_letter = key[0].lower()
if first_letter in self._first_letters:
self._raise_duplicate_key_error(key)
self._first_letters[first_letter] = key
_menu[key] = value
super().__init__(_menu)
def _raise_duplicate_key_error(self, key):
raise ValueError(
f"'{key}' is invalid."
" All pizzas must have unique first letters"
)
La classe hérite désormais de dict
. Le ._raise_duplicate_key_error()
est identique à la version dans PizzaMenu
que vous avez écrit plus tôt. La méthode .__init__()
présente quelques modifications :
- Le dictionnaire interne n'est plus un attribut de données
self._menu
mais une variable locale_menu
puisqu'elle n'est plus nécessaire ailleurs dans la classe. - Cette variable locale
._menu
est passée à l'initialiseurdict
en utilisantsuper()
dans la dernière ligne.
Puisqu'un objet PizzaMenuDict
est un dictionnaire, vous pouvez accéder aux données du dictionnaire directement via l'objet en utilisant self
dans les méthodes. Toutes les opérations sur self
utiliseront les méthodes définies dans PizzaMenuDict
. Cependant, si les méthodes ne sont pas définies dans PizzaMenuDict
, les méthodes dict
sont utilisées.
Par conséquent, PizzaMenuDict
est désormais un dictionnaire qui garantit qu'il n'y a aucun élément commençant par la même lettre lors de l'initialisation de l'objet. Il possède également un attribut de données supplémentaire, ._first_letters
. Vous pouvez confirmer que l'initialisation fonctionne comme prévu :
>>> from pizza_menu import PizzaMenuDict
>>> menu = PizzaMenuDict({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
{'Margherita': 9.5, 'Pepperoni': 10.5}
>>> menu = PizzaMenuDict({"Margherita": 9.5, "Meat Feast": 10.5})
Traceback (most recent call last):
...
ValueError: 'Meat Feast' is an invalid name.
All pizzas must have unique first letters
Vous obtenez une erreur lorsque vous tentez de créer un objet PizzaMenuDict
avec deux pizzas commençant par M. Cependant, aucune des autres méthodes spéciales n’est définie. Par conséquent, cette classe ne possède pas encore toutes les fonctionnalités requises :
>>> menu["m"]
Traceback (most recent call last):
...
KeyError: 'm'
Vous ne pouvez pas accéder à une valeur en utilisant une seule lettre. Mais vous pouvez implémenter la méthode .__getitem__()
, qui est similaire mais pas identique à la méthode que vous avez définie dans la section précédente dans PizzaMenu
:
class PizzaMenuDict(dict):
# ...
def __getitem__(self, key):
if key not in self and len(key) > 1:
raise KeyError(key)
key = self._first_letters.get(key[0].lower(), key)
return super().__getitem__(key)
Il y a deux différences par rapport au .__getitem__()
dans PizzaMenu
dans la section précédente puisque l'attribut de données ._menu
n'est plus présent dans cette version :
- L'instruction
if
dansPizzaMenu.__getitem__()
vérifie sikey
est membre deself._menu
. Cependant, l'instruction conditionnelle équivalente dansPizzaMenuDict.__getitem__()
vérifie l'appartenance directement dansself
. - L'instruction
return
dansPizzaMenu.__getitem__()
renvoieself._menu[key]
. Cependant, la dernière ligne dePizzaMenuDict.__getitem__()
appelle et renvoie la méthode spéciale.__getitem__()
de la superclasse en utilisant laclé
modifiée. La superclasse estdict
.
Ainsi, la méthode .__getitem__()
dans PizzaMenuDict
traite la casse d'une seule lettre et appelle ensuite .__getitem__()
dans le dict
classe.
Vous remarquerez le même modèle dans .__setitem__()
:
class PizzaMenuDict(dict):
# ...
def __setitem__(self, key, value):
first_letter = key[0].lower()
if len(key) == 1:
key = self._first_letters.get(first_letter, key)
if key in self:
super().__setitem__(key, value)
elif first_letter in self._first_letters:
self._raise_duplicate_key_error(key)
else:
self._first_letters[first_letter] = key
super().__setitem__(key, value)
Chaque fois que vous devez mettre à jour les données dans le mappage, vous appelez dict.__setitem__()
au lieu de définir les valeurs dans l'attribut de données ._menu
, comme vous l'avez fait dans Menu Pizza
.
Vous devez également définir .__delitem__()
de la même manière :
class PizzaMenuDict(dict):
# ...
def __delitem__(self, key):
if key not in self and len(key) > 1:
raise KeyError(key)
key = self._first_letters.pop(key[0].lower(), key)
super().__delitem__(key)
La dernière ligne de la méthode appelle la méthode .__delitem__()
dans dict
.
Notez que vous n'avez pas besoin de définir des méthodes spéciales telles que .__repr__()
, .__str__()
, .__iter__()
ou .__len__()
, comme vous deviez le faire lors de l'héritage des classes de base abstraites Mapping
et MutableMapping
. Puisqu'un PizzaMenuDict
est une sous-classe de dict
, vous pouvez vous fier aux méthodes du dictionnaire si vous n'avez pas besoin d'un comportement différent. Vous devrez démarrer une nouvelle session REPL puisque vous avez modifié la définition de la classe :
>>> from pizza_menu import PizzaMenuDict
>>> menu = PizzaMenuDict({"Margherita": 9.5, "Pepperoni": 10.5})
>>> for pizza in menu:
... print(pizza)
...
Margherita
Pepperoni
>>> menu
{'Margherita': 9.5, 'Pepperoni': 10.5}
>>> len(menu)
2
L'itération utilise la méthode .__iter__()
dans dict
. La représentation sous forme de chaîne et la recherche de la longueur de l'objet fonctionnent également comme prévu puisque les méthodes spéciales .__repr__()
et .__len__()
dans dict
sont suffisantes.
Méthodes nécessitant une mise à jour
Il semble que moins de travail soit nécessaire pour hériter de dict
. Cependant, il existe d’autres méthodes auxquelles vous devez prêter attention. Par exemple, vous pouvez explorer .pop()
avec PizzaMenuDict
:
>>> menu.pop("m")
Traceback (most recent call last):
...
KeyError: 'm'
Puisque vous n'avez pas défini .pop()
dans PizzaMenuDict
, la classe utilise la méthode dict
à la place. Cependant, dict.pop()
utilise dict.__getitem__()
, il contourne donc la méthode .__getitem__()
que vous avez définie spécifiquement pour PizzaMenuDict
. Vous devez remplacer .pop()
dans PizzaMenuDict
:
class PizzaMenuDict(dict):
# ...
def pop(self, key):
key = self._first_letters.pop(key[0].lower(), key)
return super().pop(key)
Vous vous assurez que la key
est toujours le nom complet de la pizza avant d'appeler et de renvoyer la méthode .pop()
de la superclasse. Vous pouvez confirmer que cela fonctionne dans une nouvelle session REPL :
>>> from pizza_menu import PizzaMenuDict
>>> menu = PizzaMenuDict({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu.pop("m")
9.5
>>> menu
{'Pepperoni': 10.5}
Maintenant, vous pouvez utiliser un argument d'une seule lettre dans .pop()
.
Vous devrez parcourir toutes les méthodes dict
pour déterminer celles qui doivent être définies dans PizzaMenuDict
. Vous pouvez essayer de suivre ce cours comme un exercice. Vous remarquerez que ce processus rend cette approche plus longue et plus sujette aux erreurs. Par conséquent, dans cet exemple de pizzeria, hériter directement de MutableMapping
peut être la meilleure option.
Cependant, vous pouvez avoir d'autres applications dans lesquelles vous étendez les fonctionnalités d'un dictionnaire sans modifier aucune de ses caractéristiques existantes. Hériter de dict
peut être l'option idéale dans ces cas.
Une autre alternative : collections.UserDict
Dans le module collections
, vous trouverez une autre classe dont vous pouvez hériter pour créer un objet de type dictionnaire. Vous pouvez hériter de UserDict
au lieu de MutableMapping
ou de dict
. UserDict
a été inclus dans Python alors qu'il était impossible d'hériter directement de dict
. Cependant, UserDict
n'est pas entièrement obsolète maintenant que le sous-classement de dict
est possible.
UserDict
crée un wrapper autour d'un dictionnaire plutôt que de sous-classer dict
. Un objet UserDict
inclut un attribut appelé .data
, qui est un dictionnaire contenant les données. Cet attribut est similaire à l'attribut ._menu
que vous avez ajouté à PizzaMenu
lors de l'héritage de Mapping
et de MutableMapping
.
Cependant, UserDict
est une classe concrète, pas une classe de base abstraite. Ainsi, vous n’avez pas besoin de définir les méthodes spéciales requises, sauf si vous avez besoin d’un comportement différent.
Vous avez déjà écrit deux versions du cours pour créer un menu pour la pizzeria, vous n’en écrirez donc pas une troisième dans ce tutoriel. Il n’y a pas grand chose de plus à apprendre sur les mappages en procédant ainsi. Cependant, si vous souhaitez en savoir plus sur les similitudes et les différences entre l'héritage de dict
ou UserDict
, vous pouvez lire Dictionnaires Python personnalisés : héritage de dict vs UserDict.
Conclusion
Le dictionnaire Python est le mappage le plus couramment utilisé et il conviendra dans la plupart des cas où vous avez besoin d'un mappage. Cependant, il existe d'autres mappages dans la bibliothèque standard et dans les bibliothèques tierces. Vous pouvez également avoir des applications dans lesquelles vous devez créer un mappage personnalisé.
Dans ce didacticiel, vous avez découvert :
- Caractéristiques de base d'un mappage
- Opérations qui sont communes à la plupart des mappages
- Classes de base abstraites
Mapping
etMutableMapping
- Mappages mutables et immuables définis par l'utilisateur et comment les créer
Comprendre les traits communs à tous les mappages et ce qui se passe en coulisses lorsque vous créez et utilisez des objets de mappage vous aidera à utiliser les dictionnaires et autres mappages plus efficacement dans vos programmes Python.