web-dev-qa-db-fra.com

Pourquoi la création de dict en python à partir de la liste des tuples est-elle trois fois plus lente qu'en kwargs?

Il existe deux manières de construire un dictionnaire en python, par exemple:

keyvals = [('foo', 1), ('bar', 'bar'), ('baz', 100)]

dict(keyvals)

et

dkwargs = {'foo': 1, 'bar': 'bar', 'baz': 100}

dict(**dkwargs)

Lorsque vous comparez ces

In [0]: %timeit dict(keyvals)
667 ns ± 38 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [1]: %timeit dict(**dkwargs)
225 ns ± 7.09 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

vous voyez que la première méthode est presque 3 fois plus lente que la seconde. Pourquoi est-ce?

9
nardeas

dict(**kwargs) passe dans un dictionnaire prêt à l'emploi, de sorte que Python peut simplement copier une structure interne déjà existante.

Une liste de n-uplets, quant à elle, nécessite une itération, une validation, un hachage et un positionnement des résultats dans un tableau vide et vide. Ce n'est pas aussi rapide.

Un dictionnaire Python est implémenté en tant que table de hachage , et est développé dynamiquement à mesure que des clés sont ajoutées au fil du temps; elles commencent au début et lorsque le besoin s'en fait sentir, une nouvelle table de hachage plus grande est construite, les données (clés, valeurs et hachages) sont copiées. Tout cela est invisible dans le code Python, mais le redimensionnement prend du temps. Mais lorsque vous utilisez dict(**kwargs) (ou dict(other_dict), CPython (l'implémentation Python par défaut avec laquelle vous testiez) peut prendre un raccourci: démarrer avec une table de hachage suffisamment grande immédiatement. tuples, parce que vous ne pouvez pas savoir à l’avance s’il n’y aura pas de clés en double dans la séquence.

Pour plus de détails, voir le code source C du type dict, en particulier dict_update_common implementation (appelé à partir de dict_init() ); ceci appelle soit PyDict_MergeFromSeq2() pour le cas de séquence-de-tuples, soit appelle PyDict_Merge() lorsque les arguments de mot clé sont passés.

La fonction PyDict_MergeFromSeq2() itère sur la séquence, teste chaque résultat pour s'assurer qu'il y a deux éléments, puis appelle essentiellement .__setitem__(key, value) dans le dictionnaire. Cela peut nécessiter de redimensionner le dictionnaire à un moment donné!

La fonction PyDict_Merge() (via dict_merge()) détecte spécifiquement si un dictionnaire standard a été passé, puis exécute un raccourci qui redimensionne les structures internes une fois, puis copie directement à travers les hachages et la structure du dictionnaire d'origine utiliser des appels insertdict() (suivez le chemin override == 1, car override a été défini sur 1 lorsque le dictionnaire cible est vide, ce qui est toujours le cas pour dict(**kwargs)). Redimensionner une fois et utiliser directement les données internes est beaucoup plus rapide, il reste beaucoup moins de travail à faire!

Tout ceci est un détail d'implémentation spécifique à CPython. D'autres implémentations Python telles que Jython, IronPython et PyPy peuvent prendre leurs propres décisions sur le fonctionnement des éléments internes du type dict et afficheront des différences de performances différentes pour les mêmes opérations. 

14
Martijn Pieters

Réponse courte (TL; DR)

En effet, dans le premier test, l'implémentation de dict dans CPython créera un nouveau dict à partir de la liste, mais le second copie uniquement le dictionnaire. La copie prend moins de temps que l'analyse de la liste. 

Information additionnelle

Considérons ce code:

import dis
dis.dis("dict([('foo', 1), ('bar', 'bar'), ('baz', 100)])", depth=10)
print("------------")
dis.dis("dict({'foo': 1, 'bar': 'bar', 'baz': 100})", depth=10)

Où 

Le module dis prend en charge l'analyse du bytecode CPython par le démonter.

Ce qui nous permet de voir les opérations de bytecode effectuées. Spectacles de sortie

  1           0 LOAD_NAME                0 (dict)
              2 LOAD_CONST               0 (('foo', 1))
              4 LOAD_CONST               1 (('bar', 'bar'))
              6 LOAD_CONST               2 (('baz', 100))
              8 BUILD_LIST               3
             10 CALL_FUNCTION            1
             12 RETURN_VALUE
------------
  1           0 LOAD_NAME                0 (dict)
              2 LOAD_CONST               0 (1)
              4 LOAD_CONST               1 ('bar')
              6 LOAD_CONST               2 (100)
              8 LOAD_CONST               3 (('foo', 'bar', 'baz'))
             10 BUILD_CONST_KEY_MAP      3
             12 CALL_FUNCTION            1
             14 RETURN_VALUE

De la sortie, vous pouvez voir:

  1. Les deux appels doivent charger le nom dict qui sera appelé.
  2. Après cela, la première méthode charge une liste en mémoire (BUILD_LIST) tandis que la seconde construit un dictionnaire (BUILD_CONST_KEY_MAP) (voir ici )
  3. Pour cette raison, lorsque la fonction dict est appelée (l’étape CALL_FUNCTION (voir ici )), elle est beaucoup plus courte dans le second cas, car le dictionnaire a déjà été créé, il en fait une copie au lieu de devoir parcourez la liste pour créer une table de hachage.

Note: avec le pseudo-code, vous ne pouvez pas décider de manière concluante que CALL_FUNCTION le fait, car son implémentation est écrite en C et vous ne pouvez le savoir qu'en lisant (voir la réponse de Martijn Pieters pour plus de détails). comment cette partie fonctionne). Cependant, il est utile de voir comment l’objet dictionnaire est déjà créé extérieurdict() (pas à pas, pas du point de vue syntaxique dans l’exemple), alors que pour la liste, ce n’est pas le cas.

Modifier

Pour être clair, quand vous dites

Il y a plusieurs façons de construire un dictionnaire en python

C'est vrai qu'en faisant:

dkwargs = {'foo': 1, 'bar': 'bar', 'baz': 100}

Vous créez un dictionnaire, en ce sens que l'interprète transforme une expression en un objet dictionnaire stocké en mémoire et fait pointer la variable dkwargs. Cependant, en faisant: dict(**kwargs) ou si vous préférez dict(kwargs), vous n'êtes pas vraiment créer un dictionnaire _, mais juste en copiant un objet déjà existant (et il est important de souligner en copiant)): 

>>> dict(dkwargs) is dkwargs
False

dict(kwargs) force Python à créer un nouvel objet; cependant, cela ne signifie pas qu'il doit reconstruire} _ l'objet. En fait, cette opération est inutile car, dans la pratique, ce sont des objets égaux (mais pas le même objet).

>>> id(dkwargs)
2787648914560
>>> new_dict = dict(dkwargs)
>>> id(new_dict)
2787652299584
>>> new_dict == dkwargs
True
>>> id(dkwargs) is id(new_dict)
False

Où id:

Renvoie «l'identité» d'un objet. C'est un entier qui est garanti d'être unique et constant pour cet objet tout au long de sa vie [...]

CPython implémentation détail: il s'agit de l'adresse de l'objet en mémoire.

À moins, bien sûr, que vous souhaitiez dupliquer spécifiquement l'objet pour en modifier un, les modifications ne sont pas liées à l'autre référence.

3
J. C. Rocamonde

dkwargs est déjà un dictionnaire, vous pouvez donc en faire une copie. C'est pourquoi c'est tellement plus rapide.

0
Piotr Bialoglowy