web-dev-qa-db-fra.com

Pourquoi l'ordre dans les dictionnaires et les ensembles est-il arbitraire?

Je ne comprends pas comment boucler sur un dictionnaire ou définir dans python se fait par ordre "arbitraire".

Je veux dire, c'est un langage de programmation, donc tout dans le langage doit être déterminé à 100%, n'est-ce pas? Python doit avoir une sorte d'algorithme qui décide quelle partie du dictionnaire ou de l'ensemble est choisie, 1ère, seconde et ainsi de suite.

Qu'est-ce que je rate?

142
Edgar Aroutiounian

L'ordre n'est pas arbitraire, mais dépend de l'historique d'insertion et de suppression du dictionnaire ou de l'ensemble, ainsi que de l'implémentation spécifique de Python. Pour le reste de cette réponse, pour "dictionnaire", vous pouvez également lire "set"; les ensembles sont implémentés sous forme de dictionnaires avec juste des clés et aucune valeur.

Les clés sont hachées et des valeurs de hachage sont affectées aux emplacements d'une table dynamique (elle peut augmenter ou diminuer en fonction des besoins). Et ce processus de mappage peut conduire à des collisions, ce qui signifie qu'une clé devra être insérée dans un emplacement - suivant en fonction de ce qui existe déjà.

La liste des boucles de contenu sur les emplacements, et donc les clés sont répertoriées dans l'ordre où elles actuellement résident dans le tableau.

Prenez les clés 'foo' Et 'bar', Par exemple, et supposons que la taille de la table est de 8 emplacements. Dans Python 2.7, hash('foo') est -4177197833195190597, hash('bar') est 327024216814240868. Modulo 8, cela signifie que ces deux touches sont insérées dans les emplacements 3 et 4 puis:

>>> hash('foo')
-4177197833195190597
>>> hash('foo') % 8
3
>>> hash('bar')
327024216814240868
>>> hash('bar') % 8
4

Cela informe leur ordre d'inscription:

>>> {'bar': None, 'foo': None}
{'foo': None, 'bar': None}

Tous les emplacements, à l'exception de 3 et 4, sont vides. En parcourant le tableau, vous voyez d'abord l'emplacement 3, puis l'emplacement 4, donc 'foo' Est répertorié avant 'bar'.

bar et baz, cependant, ont des valeurs de hachage qui sont exactement 8 à part et mappent donc au même emplacement exact, 4:

>>> hash('bar')
327024216814240868
>>> hash('baz')
327024216814240876
>>> hash('bar') % 8
4
>>> hash('baz') % 8
4

Leur ordre dépend maintenant de la clé qui a été insérée en premier; la deuxième clé devra être déplacée vers un emplacement suivant:

>>> {'baz': None, 'bar': None}
{'bar': None, 'baz': None}
>>> {'bar': None, 'baz': None}
{'baz': None, 'bar': None}

L'ordre des tables diffère ici, car l'une ou l'autre clé a été insérée en premier.

Le nom technique de la structure sous-jacente utilisée par CPython (l'implémentation Python la plus utilisée) est un table de hachage , celui qui utilise l'adressage ouvert. Si vous êtes curieux et comprenez assez bien C, jetez un œil à implémentation C pour tous les détails (bien documentés). Vous pouvez également regarder cette présentation Pycon 2010 par Brandon Rhodes sur le fonctionnement de CPython dict, ou récupérer une copie de Beautiful Code , qui comprend un chapitre sur l'implémentation écrite par Andrew Kuchling.

Notez que depuis Python 3.3, une graine de hachage aléatoire est également utilisée, ce qui rend les collisions de hachage imprévisibles pour empêcher certains types de déni de service (lorsqu'un attaquant rend un serveur Python non réactif par causant des collisions de hachage de masse). Cela signifie que l'ordre d'un dictionnaire donné est alors aussi dépendant de la graine de hachage aléatoire pour l'invocation actuelle de Python.

D'autres implémentations sont libres d'utiliser une structure différente pour les dictionnaires, à condition qu'elles satisfassent à l'interface documentée Python pour elles, mais je crois que toutes les implémentations jusqu'à présent utilisent une variation de la table de hachage.

CPython 3.6 introduit une implémentation nouveaudict qui maintient l'ordre d'insertion et est plus rapide et plus efficace en mémoire pour démarrer. Plutôt que de conserver une grande table clairsemée où chaque ligne fait référence à la valeur de hachage stockée et aux objets clé et valeur, la nouvelle implémentation ajoute un hachage plus petit array qui ne fait référence qu'aux indices dans une table dense contient uniquement autant de lignes qu'il y a de paires clé-valeur réelles), et c'est la table dense qui arrive à répertorier les éléments contenus dans l'ordre. Voir proposition à Python-Dev pour plus de détails . Notez que dans Python 3.6 ceci est considéré comme un détail d'implémentation, Python-le-langage ne spécifie pas que les autres implémentations doivent conserver l'ordre. Cela a changé dans Python 3.7, où ce détail était élevé pour être une spécification de langue ; pour que toute implémentation soit correctement compatible avec Python 3.7 ou une version plus récente, elle doit copier ce comportement préservant l'ordre.

Python 2.7 et plus récent fournit également une OrderedDict class , une sous-classe de dict qui ajoute une structure de données supplémentaire pour enregistrer l'ordre des clés. Au prix d'une certaine vitesse et de mémoire supplémentaire, cette classe se souvient dans quel ordre vous avez inséré les clés; la liste des clés, des valeurs ou des éléments le fera alors dans cet ordre. Il utilise une liste doublement liée stockée dans un dictionnaire supplémentaire pour maintenir la commande à jour efficacement. Voir le article de Raymond Hettinger décrivant l'idée . Notez que le type set n'est toujours pas ordonné.

Si vous vouliez un ensemble ordonné, vous pouvez installer le oset package ; cela fonctionne sur Python 2.5 et plus.

224
Martijn Pieters

Il s'agit plus d'une réponse à Python 3.41 A set avant sa fermeture en tant que doublon.


Les autres ont raison: ne vous fiez pas à la commande. Ne prétendez même pas qu'il y en a un.

Cela dit, il y a un chose sur laquelle vous pouvez compter:

list(myset) == list(myset)

Autrement dit, l'ordre est stable.


Pour comprendre pourquoi il existe un ordre perçu, il faut comprendre certaines choses:

  • Cela Python utilise ensembles de hachage,

  • Comment l'ensemble de hachage de CPython est stocké en mémoire et

  • Comment les nombres sont hachés

Du haut:

A ensemble de hachage est une méthode de stockage de données aléatoires avec des temps de recherche très rapides.

Il a un tableau de support:

# A C array; items may be NULL,
# a pointer to an object, or a
# special dummy object
_ _ 4 _ _ 2 _ _ 6

Nous ignorerons l'objet factice spécial, qui n'existe que pour faciliter le traitement des suppressions, car nous ne supprimerons pas ces ensembles.

Afin d'avoir une recherche vraiment rapide, vous faites de la magie pour calculer un hachage à partir d'un objet. La seule règle est que deux objets égaux ont le même hachage. (Mais si deux objets ont le même hachage, ils peuvent être inégaux.)

Vous faites ensuite de l'index en prenant le module par la longueur du tableau:

hash(4) % len(storage) = index 2

Cela rend l'accès aux éléments très rapide.

Les hachages ne représentent que la majeure partie de l'histoire, car hash(n) % len(storage) et hash(m) % len(storage) peuvent donner le même nombre. Dans ce cas, plusieurs stratégies différentes peuvent essayer de résoudre le conflit. CPython utilise le "palpage linéaire" 9 fois avant de faire des choses compliquées, il cherchera donc à gauche de la fente jusqu'à 9 emplacements avant de chercher ailleurs.

Les ensembles de hachage de CPython sont stockés comme ceci:

  • Un ensemble de hachage peut être pas plus de 2/3 plein . S'il y a 20 éléments et que le tableau de supports est de 30 éléments, le magasin de supports sera redimensionné pour être plus grand. En effet, vous obtenez plus souvent des collisions avec de petits magasins de support, et les collisions ralentissent tout.

  • Le magasin de sauvegarde est redimensionné en puissances de 4, à partir de 8, à l'exception des grands ensembles (éléments 50k) qui se redimensionnent en puissances de deux: (8, 32, 128, ...).

Ainsi, lorsque vous créez un tableau, le magasin de sauvegarde est de longueur 8. Lorsqu'il est plein et que vous ajoutez un élément, il contiendra brièvement 6 éléments. 6 > ²⁄₃·8 Donc cela déclenche un redimensionnement, et le magasin de sauvegarde quadruples à la taille 32.

Enfin, hash(n) renvoie simplement n pour les nombres (sauf -1 Qui est spécial).


Alors, regardons le premier:

v_set = {88,11,1,33,21,3,7,55,37,8}

len(v_set) est 10, donc le magasin de sauvegarde est au moins 15 (+1) après que tous les éléments ont été ajoutés . La puissance pertinente de 2 est 32. Le magasin de sauvegarde est donc:

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __

On a

hash(88) % 32 = 24
hash(11) % 32 = 11
hash(1)  % 32 = 1
hash(33) % 32 = 1
hash(21) % 32 = 21
hash(3)  % 32 = 3
hash(7)  % 32 = 7
hash(55) % 32 = 23
hash(37) % 32 = 5
hash(8)  % 32 = 8

donc ces insérer comme:

__  1 __  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __
   33 ← Can't also be where 1 is;
        either 1 or 33 has to move

Nous attendons donc une commande comme

{[1 or 33], 3, 37, 7, 8, 11, 21, 55, 88}

avec le 1 ou 33 qui n'est pas au début ailleurs. Cela utilisera le sondage linéaire, nous aurons donc:

       ↓
__  1 33  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __

ou

       ↓
__ 33  1  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __

Vous pourriez vous attendre à ce que le 33 soit celui qui a été déplacé parce que le 1 était déjà là, mais en raison du redimensionnement qui se produit lors de la construction de l'ensemble, ce n'est pas vraiment le cas. Chaque fois que l'ensemble est reconstruit, les éléments déjà ajoutés sont effectivement réorganisés.

Vous voyez maintenant pourquoi

{7,5,11,1,4,13,55,12,2,3,6,20,9,10}

pourrait être en ordre. Il y a 14 éléments, donc le magasin de sauvegarde est au moins 21 + 1, ce qui signifie 32:

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __

1 à 13 hachage dans les 13 premiers emplacements. 20 va dans l'emplacement 20.

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ __ __ __ __ __ __ __ __ __

55 va dans l'emplacement hash(55) % 32 qui est 23:

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ 55 __ __ __ __ __ __ __ __

Si nous avons choisi 50 à la place, nous nous attendrions

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ 50 __ 20 __ __ __ __ __ __ __ __ __ __ __

Et voilà:

{1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 20, 50}
#>>> {1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 50, 20}

pop est implémenté tout simplement par l'apparence des choses: il parcourt la liste et fait apparaître la première.


C'est tout le détail de l'implémentation.

36
Veedrac

"Arbitraire" n'est pas la même chose que "non déterminé".

Ce qu'ils disent, c'est qu'il n'y a pas de propriétés utiles de l'ordre d'itération du dictionnaire qui sont "dans l'interface publique". Il existe presque certainement de nombreuses propriétés de l'ordre d'itération qui sont entièrement déterminées par le code qui implémente actuellement l'itération de dictionnaire, mais les auteurs ne vous les promettent pas comme quelque chose que vous pouvez utiliser. Cela leur donne plus de liberté pour changer ces propriétés entre les versions Python (ou même juste dans différentes conditions de fonctionnement, ou complètement au hasard à l'exécution) sans craindre que votre programme ne se casse.

Ainsi, si vous écrivez un programme qui dépend de n'importe quelle propriété de l'ordre du dictionnaire, alors vous "rompez le contrat" ​​d'utilisation du type de dictionnaire et du Python les développeurs ne promettent pas que cela fonctionnera toujours, même si cela semble fonctionner pour l'instant lorsque vous le testez. C'est essentiellement l'équivalent de s'appuyer sur un "comportement non défini" en C.

16
Ben

Les autres réponses à cette question sont excellentes et bien écrites. Le PO demande "comment" que j'interprète comme "comment s'en tirer" ou "pourquoi".

La documentation Python indique que dictionnaires ne sont pas commandés car le dictionnaire Python implémente le type de données abstrait - tableau associatif . Comme on dit

l'ordre dans lequel les liaisons sont renvoyées peut être arbitraire

En d'autres termes, un étudiant en informatique ne peut pas supposer qu'un tableau associatif est ordonné. La même chose est vraie pour les ensembles dans math

l'ordre dans lequel les éléments d'un ensemble sont répertoriés n'est pas pertinent

et informatique

un ensemble est un type de données abstrait qui peut stocker certaines valeurs, sans ordre particulier

L'implémentation d'un dictionnaire à l'aide d'une table de hachage est un détail d'implémentation qui est intéressant en ce qu'il a les mêmes propriétés que les tableaux associatifs en ce qui concerne l'ordre.

6
John Schmitt

Python utilise ( table de hachage pour stocker les dictionnaires, il n'y a donc pas d'ordre dans les dictionnaires ou autres objets itérables qui utilisent la table de hachage.

Mais en ce qui concerne les indices des éléments dans un objet de hachage, python calculez les indices en fonction du code suivant dans hashtable.c :

key_hash = ht->hash_func(key);
index = key_hash & (ht->num_buckets - 1);

Par conséquent, comme la valeur de hachage des entiers est l'entier lui-même* l'index est basé sur le nombre (ht->num_buckets - 1 est une constante) donc l'index calculé par Bitwise-et entre (ht->num_buckets - 1) et le nombre lui-même* (attendez-vous à -1 dont le hachage est -2), et aux autres objets avec leur valeur de hachage.

considérons l'exemple suivant avec set qui utilise la table de hachage:

>>> set([0,1919,2000,3,45,33,333,5])
set([0, 33, 3, 5, 45, 333, 2000, 1919])

Pour le numéro 33, Nous avons:

33 & (ht->num_buckets - 1) = 1

C'est en fait:

'0b100001' & '0b111'= '0b1' # 1 the index of 33

Remarque dans ce cas, (ht->num_buckets - 1) Est 8-1=7 Ou 0b111.

Et pour 1919:

'0b11101111111' & '0b111' = '0b111' # 7 the index of 1919

Et pour 333:

'0b101001101' & '0b111' = '0b101' # 5 the index of 333

Pour plus de détails sur python fonction de hachage, il est bon de lire les citations suivantes de code source python :

Principales subtilités à venir: La plupart des schémas de hachage dépendent de la présence d'une "bonne" fonction de hachage, dans le sens de la simulation de l'aléatoire. Python ne fonctionne pas: ses fonctions de hachage les plus importantes (pour les chaînes et les entiers) sont très régulières dans les cas courants:

>>> map(hash, (0, 1, 2, 3))
  [0, 1, 2, 3]
>>> map(hash, ("namea", "nameb", "namec", "named"))
  [-1658398457, -1658398460, -1658398459, -1658398462]

Ce n'est pas forcément mauvais! Au contraire, dans une table de taille 2 ** i, prendre les i bits de poids faible comme index de table initial est extrêmement rapide, et il n'y a pas de collision du tout pour les dits indexés par une plage contiguë d'ints. La même chose est approximativement vraie lorsque les clés sont des chaînes "consécutives". Cela donne donc un comportement meilleur que aléatoire dans les cas courants, et c'est très souhaitable.

OTOH, lorsque des collisions se produisent, la tendance à remplir des tranches contiguës de la table de hachage rend cruciale une bonne stratégie de résolution des collisions. Prendre uniquement les i derniers bits du code de hachage est également vulnérable: par exemple, considérez la liste [i << 16 for i in range(20000)] comme un ensemble de clés. Puisque les entiers sont leurs propres codes de hachage, et cela correspond à un dict de taille 2 ** 15, les 15 derniers bits de chaque code de hachage sont tous à 0: ils tous mappe au même index de table.

Mais la restauration de cas inhabituels ne devrait pas ralentir les cas habituels, nous prenons donc tout de même les derniers bits. C'est à la résolution des collisions de faire le reste. Si nous généralement trouvons la clé que nous recherchons au premier essai (et, il s'avère que nous le faisons généralement - le facteur de charge de la table est maintenu sous 2/3, donc les cotes sont solidement en notre faveur), alors il est préférable de garder la saleté de calcul de l'indice initial à bon marché.


* La fonction de hachage pour la classe int:

class int:
    def __hash__(self):
        value = self
        if value == -1:
            value = -2
        return value
5
Kasrâmvd

Commençant par Python 3.7 (et déjà dans CPython 3.6 ), les éléments du dictionnaire restent dans l'ordre où ils ont été insérés .

1
Boris