J'ai récemment comparé les vitesses de traitement de []
et de list()
et j'ai été surpris de découvrir que []
s'exécute plus de trois fois plus rapidement que list()
. J'ai exécuté le même test avec {}
et dict()
et les résultats étaient pratiquement identiques: []
et {}
prenaient chacun environ 0,128sec/million de cycles, alors que list()
et dict()
prenaient environ 0,428sec/million cycles chacun.
Pourquoi est-ce? Est-ce que []
et {}
(et probablement ()
et ''
, aussi) renvoient immédiatement une copie d'un littéral de stock vide tandis que leurs homologues nommés explicitement (list()
, dict()
, Tuple()
, str()
) crée-t-il pleinement un objet, qu’il ait ou non des éléments?
Je n'ai aucune idée de la façon dont ces deux méthodes diffèrent, mais j'aimerais le savoir. Je ne pouvais pas trouver de réponse dans la documentation ou sur le SO, et la recherche de crochets vides s'est avérée plus problématique que prévu.
J'ai obtenu les résultats de mon chronométrage en appelant timeit.timeit("[]")
et timeit.timeit("list()")
, et timeit.timeit("{}")
et timeit.timeit("dict()")
, pour comparer les listes et les dictionnaires, respectivement. J'utilise Python 2.7.9.
J'ai récemment découvert " Pourquoi est-ce que si True est plus lent que si 1? " qui compare les performances de if True
à if 1
et semble toucher à un scénario similaire littéral/global ; Peut-être que cela vaut également la peine d'être examiné.
Parce que []
et {}
sont syntaxe littérale . Python peut créer du bytecode uniquement pour créer la liste ou les objets du dictionnaire:
>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
1 0 BUILD_LIST 0
3 RETURN_VALUE
>>> dis.dis(compile('{}', '', 'eval'))
1 0 BUILD_MAP 0
3 RETURN_VALUE
list()
et dict()
sont des objets distincts. Leurs noms doivent être résolus, la pile doit être impliquée pour pousser les arguments, la trame doit être stockée pour une récupération ultérieure et un appel doit être effectué. Cela prend plus de temps.
Pour le cas vide, cela signifie que vous avez au minimum un LOAD_NAME
(qui doit rechercher dans l’espace de nom global ainsi que dans le module __builtin__
) suivi d'un CALL_FUNCTION
, qui doit conserver le cadre actuel:
>>> dis.dis(compile('list()', '', 'eval'))
1 0 LOAD_NAME 0 (list)
3 CALL_FUNCTION 0
6 RETURN_VALUE
>>> dis.dis(compile('dict()', '', 'eval'))
1 0 LOAD_NAME 0 (dict)
3 CALL_FUNCTION 0
6 RETURN_VALUE
Vous pouvez programmer la recherche de nom séparément avec timeit
:
>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119
La différence de temps il y a probablement une collision de dictionnaire de hachage. Soustrayez ces heures des heures d'appel de ces objets et comparez le résultat avec les heures d'utilisation des littéraux:
>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125
Il faut donc 1.00 - 0.31 - 0.30 == 0.39
secondes supplémentaires par tranche de 10 millions d'appels pour appeler l'objet.
Vous pouvez éviter le coût global de la recherche en créant un alias dans les noms globaux (en utilisant une configuration timeit
, tout ce que vous associez à un nom est local):
>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137
mais vous ne pouvez jamais surmonter ce coût CALL_FUNCTION
.
list()
nécessite une recherche globale et un appel de fonction, mais []
est compilé en une seule instruction. Voir:
Python 2.7.3
>>> import dis
>>> print dis.dis(lambda: list())
1 0 LOAD_GLOBAL 0 (list)
3 CALL_FUNCTION 0
6 RETURN_VALUE
None
>>> print dis.dis(lambda: [])
1 0 BUILD_LIST 0
3 RETURN_VALUE
None
Parce que list
est un fonction pour convertir, disons une chaîne en un objet de liste, alors que []
est utilisé pour créer une liste à la volée. Essayez ceci (peut-être plus logique pour vous):
x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]
Tandis que
y = ["wham bam"]
>>> y
["wham bam"]
Vous donne une liste réelle contenant tout ce que vous y mettez.
Les réponses ici sont excellentes, pertinentes et couvrent pleinement cette question. Je laisserai tomber un peu plus loin le code octet pour ceux qui sont intéressés. J'utilise le dernier repo de CPython; Les anciennes versions se comportent de manière similaire à cet égard, mais de légères modifications pourraient être apportées.
Voici une ventilation de l'exécution pour chacun de ces éléments, _BUILD_LIST
_ pour _[]
_ et _CALL_FUNCTION
_ pour list()
.
BUILD_LIST
_:Vous devriez juste voir l'horreur:
_PyObject *list = PyList_New(oparg);
if (list == NULL)
goto error;
while (--oparg >= 0) {
PyObject *item = POP();
PyList_SET_ITEM(list, oparg, item);
}
Push(list);
DISPATCH();
_
Terriblement compliqué, je sais. Voici à quel point c'est simple:
PyList_New
(cela alloue principalement de la mémoire pour un nouvel objet liste), oparg
indiquant le nombre d'arguments de la pile. Droit au but.if (list==NULL)
.PyList_SET_ITEM
(une macro).Pas étonnant que c'est rapide! C'est fait sur mesure pour créer de nouvelles listes, rien d'autre :-)
CALL_FUNCTION
_:Voici la première chose que vous voyez lorsque vous jetez un œil à la gestion du code _CALL_FUNCTION
_:
_PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
Push(res);
if (res == NULL) {
goto error;
}
DISPATCH();
_
Semble assez inoffensif, non? Eh bien, non, malheureusement pas, call_function
n'est pas un gars simple qui appelle la fonction immédiatement, il ne peut pas. Au lieu de cela, il récupère l'objet de la pile, tous les arguments de la pile, puis bascule en fonction du type de l'objet; est-ce un:
PyCFunction_Type
? Non, c'est list
, list
n'est pas de type PyCFunction
PyMethodType
? Non, voir ci-dessus.PyFunctionType
? Nopee, voir précédent.Nous appelons le type list
, l'argument transmis à _call_function
_ est PyList_Type
. CPython doit maintenant appeler une fonction générique pour gérer tous les objets appelables nommés _PyObject_FastCallKeywords
, voire davantage d’appels de fonction.
Cette fonction vérifie à nouveau certains types de fonctions (ce que je ne comprends pas pourquoi), puis, après avoir créé un dict pour kwargs si nécessaire , passe à appel _PyObject_FastCallDict
.
__PyObject_FastCallDict
_ nous emmène enfin quelque part! Après avoir effectué encore plus de vérifications , il saisit le créneau _tp_call
_ du type
du type
que nous avons transmis c’est-à-dire qu’il saisit _type.tp_call
_. Il crée ensuite un tuple à partir des arguments passés avec __PyStack_AsTuple
_ et, enfin, n appel peut finalement être passé!
_tp_call
_, qui correspond à type.__call__
prend le relais et crée finalement l'objet liste. Il appelle les listes ___new__
_ qui correspond à PyType_GenericNew
et lui alloue de la mémoire avec PyType_GenericAlloc
: C'est en fait la partie où il rattrape _PyList_New
_, enfin . Tous les précédents sont nécessaires pour manipuler les objets de manière générique.
En fin de compte, _type_call
_ appelle _list.__init__
_ et initialise la liste avec tous les arguments disponibles, puis nous retournons dans la voie empruntée. :-)
Enfin, rappelez-vous le _LOAD_NAME
_, c’est un autre gars qui contribue ici.
Il est facile de voir que, lorsque nous traitons avec notre entrée, Python doit généralement sauter entre des cerceaux afin de déterminer la fonction C
appropriée pour effectuer le travail. Il n'a pas la courtoisie de l'appeler immédiatement car il est dynamique, quelqu'un peut masquer list
( et le garçon le fait souvent ) et un autre chemin doit être pris.
C’est là que list()
perd beaucoup: L’exploration Python doit _ faire pour savoir ce qu’elle devrait faire.
La syntaxe littérale, d'autre part, signifie exactement une chose; il ne peut pas être changé et se comporte toujours de manière prédéterminée.
Note de bas de page: Tous les noms de fonction sont susceptibles de changer d’une publication à l’autre. Le point est toujours valable et le sera très probablement dans les versions futures, c’est la recherche dynamique qui ralentit les choses.
Pourquoi
[]
est-il plus rapide quelist()
?
La principale raison est que Python traite list()
comme une fonction définie par l'utilisateur, ce qui signifie que vous pouvez l'intercepter en aliasant quelque chose d'autre en list
et en faisant quelque chose de différent (comme utiliser votre propre liste de sous-classes ou peut-être une deque).
Il crée immédiatement une nouvelle instance d'une liste intégrée avec []
.
Mon explication cherche à vous en donner l'intuition.
[]
est communément appelé syntaxe littérale.
Dans la grammaire, cela s'appelle un "affichage de liste". de la documentation :
Un affichage sous forme de liste est une série d’expressions éventuellement vides placées entre crochets:
list_display ::= "[" [starred_list | comprehension] "]"
Un affichage de liste génère un nouvel objet de liste, le contenu étant spécifié soit par une liste d'expressions, soit par une compréhension. Lorsqu'une liste d'expressions séparées par des virgules est fournie, ses éléments sont évalués de gauche à droite et placés dans l'objet de liste dans cet ordre. Lorsqu'une compréhension est fournie, la liste est construite à partir des éléments résultant de la compréhension.
En bref, cela signifie qu'un objet intégré de type list
est créé.
Il n’est pas possible de contourner cela - ce qui signifie que Python peut le faire aussi rapidement que possible.
D'autre part, list()
peut être intercepté en créant un list
intégré à l'aide du constructeur de liste intégré.
Par exemple, supposons que nous voulions que nos listes soient créées bruyamment:
class List(list):
def __init__(self, iterable=None):
if iterable is None:
super().__init__()
else:
super().__init__(iterable)
print('List initialized.')
Nous pourrions alors intercepter le nom list
sur la portée globale du module, puis créer un list
pour créer notre liste de sous-types:
>>> list = List
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>
De même, nous pourrions le supprimer de l'espace de noms global
del list
et le mettre dans le namespace intégré:
import builtins
builtins.list = List
Et maintenant:
>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>
Et notez que l’affichage de la liste crée inconditionnellement une liste:
>>> list_1 = []
>>> type(list_1)
<class 'list'>
Nous ne le faisons probablement que temporairement, alors annulons nos modifications - supprimons d’abord le nouvel objet List
des commandes intégrées:
>>> del builtins.list
>>> builtins.list
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'builtins' has no attribute 'list'
>>> list()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'list' is not defined
Oh non, nous avons perdu la trace de l'original.
Ne vous inquiétez pas, nous pouvons toujours obtenir list
- c'est le type d'un littéral de liste:
>>> builtins.list = type([])
>>> list()
[]
Alors...
Pourquoi
[]
est-il plus rapide quelist()
?
Comme nous l'avons vu - nous pouvons écraser list
- mais nous ne pouvons pas intercepter la création du type littéral. Lorsque nous utilisons list
, nous devons faire des recherches pour voir s’il ya quelque chose.
Ensuite, nous devons appeler le callable que nous avons levé. De la grammaire:
Un appel appelle un objet appelable (une fonction, par exemple) avec une série d'arguments éventuellement vide:
call ::= primary "(" [argument_list [","] | comprehension] ")"
Nous pouvons voir qu'il fait la même chose pour n'importe quel nom, pas seulement la liste:
>>> import dis
>>> dis.dis('list()')
1 0 LOAD_NAME 0 (list)
2 CALL_FUNCTION 0
4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
1 0 LOAD_NAME 0 (doesnotexist)
2 CALL_FUNCTION 0
4 RETURN_VALUE
Pour []
, il n'y a pas d'appel de fonction au niveau du Python _:
>>> dis.dis('[]')
1 0 BUILD_LIST 0
2 RETURN_VALUE
Il va tout simplement directement à la construction de la liste sans aucune recherche ou appel au niveau du bytecode.
Nous avons démontré que list
peut être intercepté avec un code utilisateur à l'aide des règles de portée, et que list()
cherche un appelable, puis l'appelle.
Alors que []
est un affichage sous forme de liste, ou littéral, et évite ainsi la recherche de nom et l'appel de fonction.