Est-ce que quelqu'un sait comment le type de dictionnaire intégré pour python est implémenté? Je pense que c'est une sorte de table de hachage, mais je n'ai pas été en mesure de trouver une réponse définitive.
Voici tout ce qui concerne Python dict que j'ai pu assembler (probablement plus que quiconque aimerait savoir; mais la réponse est exhaustive).
dict
utilise un adressage ouvert pour résoudre les collisions de hachage (expliqué ci-dessous) (voir dictobject.c: 296-297 ).O(1)
par index).La figure ci-dessous est une représentation logique d'une table de hachage Python. Dans la figure ci-dessous, 0, 1, ..., i, ...
À gauche sont des index des fentes . dans la table de hachage (ils sont juste à des fins d'illustration et ne sont pas stockés avec la table évidemment!).
# Logical model of Python Hash table
-+-----------------+
0| <hash|key|value>|
-+-----------------+
1| ... |
-+-----------------+
.| ... |
-+-----------------+
i| ... |
-+-----------------+
.| ... |
-+-----------------+
n| ... |
-+-----------------+
Lorsqu'un nouveau dict est initialisé, il commence par 8 slots. (voir dictobject.h: 49 )
i
, basé sur le hachage de la clé. CPython utilise initialement i = hash(key) & mask
(où mask = PyDictMINSIZE - 1
, Mais ce n'est pas vraiment important). Notez simplement que l'emplacement initial, i
, qui est coché, dépend du hash de la clé.<hash|key|value>
). Mais si cette fente est occupée!? Probablement parce qu'une autre entrée a le même hash (collision de hash!)==
is
comparaison) de l'entrée dans l'emplacement contre le hachage et la clé de l'entrée actuelle à insérer ( dictobject.c: 337,344-345 ) respectivement. Si les deux, il pense que l'entrée existe déjà, abandonne et passe à l'entrée suivante à insérer. Si le hachage ou la clé ne correspondent pas, le test commence .i+1, i+2, ...
Et utiliser le premier disponible (le sondage linéaire). Mais pour des raisons bien expliquées dans les commentaires (voir dictobject.c: 33-126 ), CPython utilise un sondage aléatoire . Dans le sondage aléatoire, la tranche suivante est sélectionnée dans un ordre pseudo-aléatoire. L'entrée est ajoutée au premier emplacement vide. Pour cette discussion, l’algorithme utilisé pour choisir l’emplacement suivant n’est pas vraiment important (voir dictobject.c: 33-126 pour l’algorithme de vérification). Ce qui est important, c'est que les slots soient sondés jusqu'à ce que le premier slot vide soit trouvé.dict
sera redimensionné s’il est plein aux deux tiers. Cela évite de ralentir les recherches. (voir dictobject.h: 64-65 )NOTE: J'ai fait la recherche sur Python Implémentation de dict en réponse à ma propre question à propos de la façon dont plusieurs entrées dans un dict peuvent avoir les mêmes valeurs de hachage. J'ai posté une édition légèrement modifiée. version de la réponse ici parce que toute la recherche est très pertinente pour cette question aussi.
Les dictionnaires Python utilisent adressage ouvert ( référence dans Beautiful code )
NB! Adressage ouvert, alias hachage fermé devrait, comme indiqué dans Wikipedia , ne pas confondre avec son contraire open hashing !
L'adressage ouvert signifie que le dict utilise des emplacements de tableau et que, lorsque la position principale d'un objet est prise dans le dict, la tache de l'objet est recherchée à un index différent dans le même tableau, à l'aide d'un schéma de "perturbation", dans lequel la valeur de hachage de l'objet joue un rôle. .
Comment les dictionnaires intégrés de Python sont-ils implémentés?
Voici le cours court:
L'aspect ordonné est non officiel à partir de Python 3.6, mais officiel dans Python 3,7 ).
Pendant longtemps, cela a fonctionné exactement comme ça. Python préalloue 8 lignes vides et utilise le hachage pour déterminer où coller la paire clé-valeur. Par exemple, si le hachage de la clé se termine en 001, il le restera dans le 1 index (comme dans l'exemple ci-dessous.)
hash key value
null null null
...010001 ffeb678c 633241c4 # addresses of the keys and values
null null null
... ... ...
Chaque ligne occupe 24 octets sur une architecture 64 bits et 12 sur 32 bits. (Notez que les en-têtes de colonne ne sont que des étiquettes - elles n'existent pas en mémoire.)
Si le hachage se termine de la même manière que le hachage d'une clé préexistante, il s'agit d'une collision. La paire clé-valeur est alors collée à un emplacement différent.
Une fois que 5 valeurs-clés sont stockées, lors de l'ajout d'une autre paire clé-valeur, la probabilité de collisions de hachage est trop grande, de sorte que la taille du dictionnaire est doublée. Dans un processus 64 bits, avant le redimensionnement, nous avons 72 octets vides et après, nous perdons 240 octets en raison des 10 lignes vides.
Cela prend beaucoup de place, mais le temps de recherche est relativement constant. L'algorithme de comparaison de clé consiste à calculer le hachage, à accéder à l'emplacement attendu, à comparer l'identifiant de la clé. S'ils sont identiques, ils sont égaux. Si ce n'est pas le cas, comparez les valeurs de hachage, si elles sont et non identiques, elles ne sont pas égales. Sinon, nous comparons enfin les clés pour l’égalité et, si elles sont égales, renvoyons la valeur. La comparaison finale pour l'égalité peut être assez lente, mais les vérifications antérieures raccourcissent généralement la comparaison finale, rendant les recherches très rapides.
(Les collisions ralentissent les choses et un attaquant pourrait théoriquement utiliser des collisions de hachage pour réaliser une attaque par déni de service. Nous avons donc randomisé la fonction de hachage de sorte qu'elle calcule un hachage différent pour chaque nouveau processus Python. )
L'espace perdu décrit ci-dessus nous a amenés à modifier la mise en œuvre des dictionnaires, avec une nouvelle fonctionnalité intéressante (bien que non officielle) selon laquelle les dictionnaires sont maintenant commandés (par insertion).
Nous commençons plutôt par pré-allouer un tableau pour l'index de l'insertion.
Puisque notre première paire clé-valeur va dans le deuxième emplacement, nous indexons comme ceci:
[null, 0, null, null, null, null, null, null]
Et notre table est simplement renseignée par ordre d'insertion:
hash key value
...010001 ffeb678c 633241c4
... ... ...
Ainsi, lorsque nous recherchons une clé, nous utilisons le hachage pour vérifier la position attendue (dans ce cas, nous passons directement à l'index 1 du tableau), puis à cet index dans la table de hachage (par exemple l'index 0 ), vérifiez que les clés sont égales (en utilisant le même algorithme que celui décrit précédemment) et, dans l’affirmative, renvoyez la valeur.
Nous conservons un temps de recherche constant, avec des pertes de vitesse mineures dans certains cas et des gains dans d’autres, avec l’avantage de gagner beaucoup d’espace par rapport à la mise en œuvre préexistante. Le seul espace gaspillé sont les octets nuls du tableau d'index.
Raymond Hettinger a introduit ceci à python-dev en décembre 2012. Il est finalement entré dans CPython en Python 3.6 . La commande par insertion est toujours considérée comme un détail d'implémentation permettant aux autres implémentations de Python une chance de se rattraper.
Une autre optimisation pour économiser de l'espace est une implémentation qui partage les clés. Ainsi, au lieu d'avoir des dictionnaires redondants qui occupent tout cet espace, nous avons des dictionnaires qui réutilisent les clés partagées et leurs hachages. Vous pouvez penser comme ça:
hash key dict_0 dict_1 dict_2...
...010001 ffeb678c 633241c4 fffad420 ...
... ... ... ... ...
Pour une machine 64 bits, cela pourrait économiser jusqu'à 16 octets par clé par dictionnaire supplémentaire.
Ces clés à clé partagée sont destinées à être utilisées pour les objets personnalisés '__dict__
. Pour avoir ce comportement, je pense que vous devez finir de remplir votre __dict__
avant d'instancier votre prochain objet ( voir PEP 412 ). Cela signifie que vous devez attribuer tous vos attributs dans le __init__
ou __new__
, sinon vous pourriez ne pas économiser votre espace.
Cependant, si vous connaissez tous vos attributs au moment où votre __init__
est exécuté, vous pouvez également fournir __slots__
pour votre objet, et garantissez que __dict__
n'est pas créé du tout (s'il n'est pas disponible chez les parents), ni même permet __dict__
mais garantissez que vos attributs prévus sont quand même stockés dans des slots. Pour plus sur __slots__
, voir ma réponse ici .
**kwargs
dans une fonction.