Si j'ai bien compris, la fonction range()
, qui est en fait un type d'objet dans Python 3 , génère son contenu à la volée, à la manière d'un générateur.
Ceci étant le cas, je me serais attendu à ce que la ligne suivante prenne un temps démesuré, car pour déterminer si 1 quadrillion se situe dans la plage, il faudrait générer un quadrillion de valeurs:
1000000000000000 in range(1000000000000001)
De plus, il semble que peu importe le nombre de zéros ajoutés, le calcul prend plus ou moins le même temps (essentiellement instantané).
J'ai aussi essayé des choses comme ça, mais le calcul est encore presque instantané:
1000000000000000000000 in range(0,1000000000000000000001,10) # count by tens
Si j'essaie d'implémenter ma propre fonction de plage, le résultat n'est pas très beau !!
def my_crappy_range(N):
i = 0
while i < N:
yield i
i += 1
return
Qu'est-ce que l'objet range()
fait sous le capot qui le fait si vite?
Réponse de Martijn Pieters a été choisi pour sa complétude, mais reportez-vous également à la { première réponse de abarnert } pour une bonne discussion sur ce que signifie pour range
être une séquence complète. Python 3 et quelques informations/avertissements concernant une incohérence potentielle pour l'optimisation de la fonction __contains__
dans les implémentations Python. l'autre réponse d'abarnert entre plus en détail et fournit des liens pour ceux qui sont intéressés par l'historique de l'optimisation dans Python 3 (et le manque d'optimisation de xrange
dans Python 2). Les réponses par poke _ et par wim _ fournissent le code source C approprié et des explications aux personnes intéressées.
L'objet range()
de Python 3 ne produit pas de nombres immédiatement; c'est un objet séquence intelligent qui produit des nombres à la demande. Elle ne contient que vos valeurs de début, de fin et de pas. Lorsque vous parcourez l’objet, le nombre entier suivant est calculé à chaque itération.
L'objet implémente également le object.__contains__
hook , et calcule si votre numéro fait partie de sa plage. Le calcul est une opération O(1) à temps constant. Il n'est jamais nécessaire d'analyser tous les entiers possibles de la plage.
Depuis la documentation de l'objet range()
:
L'avantage du type
range
par rapport à unlist
ouTuple
normal est qu'un objet de plage utilisera toujours la même (petite) quantité de mémoire, quelle que soit la taille de la plage qu'il représente (puisqu'il ne stocke que les valeursstart
,stop
etstep
, calculant les articles individuels et les sous-gammes au besoin).
Donc, au minimum, votre objet range()
ferait:
class my_range(object):
def __init__(self, start, stop=None, step=1):
if stop is None:
start, stop = 0, start
self.start, self.stop, self.step = start, stop, step
if step < 0:
lo, hi = stop, start
else:
lo, hi = start, stop
self.length = ((hi - lo - 1) // abs(step)) + 1
def __iter__(self):
current = self.start
if self.step < 0:
while current > self.stop:
yield current
current += self.step
else:
while current < self.stop:
yield current
current += self.step
def __len__(self):
return self.length
def __getitem__(self, i):
if i < 0:
i += self.length
if 0 <= i < self.length:
return self.start + i * self.step
raise IndexError('Index out of range: {}'.format(i))
def __contains__(self, num):
if self.step < 0:
if not (self.stop < num <= self.start):
return False
else:
if not (self.start <= num < self.stop):
return False
return (num - self.start) % self.step == 0
Il manque encore plusieurs éléments pris en charge par une fonction range()
réelle (telles que les méthodes .index()
ou .count()
, le hachage, le test d'égalité ou le découpage en tranches), mais qui devraient vous donner une idée.
J'ai également simplifié l'implémentation __contains__
pour ne me concentrer que sur les tests de nombres entiers; Si vous attribuez à un objet range()
réel une valeur non entière (y compris les sous-classes de int
), une analyse lente est lancée pour déterminer s'il existe une correspondance, comme si vous utilisiez un test de confinement par rapport à la liste de toutes les valeurs contenues. Cela a été fait pour continuer à prendre en charge d'autres types numériques qui prennent en charge les tests d'égalité avec des entiers, mais ne sont pas censés prendre en charge l'arithmétique des entiers. Voir le problème original Python qui a implémenté le test de confinement.
Utilisez la source , Luke!
Dans CPython, range(...).__contains__
(un encapsuleur de méthode) finira par déléguer à un calcul simple qui vérifie si la valeur peut éventuellement être dans la plage. La raison de la vitesse ici est que nous utilisons un raisonnement mathématique sur les limites, plutôt qu'une itération directe de l'objet range. Pour expliquer la logique utilisée:
start
et stop
, etPar exemple, 994
est dans range(4, 1000, 2)
car:
4 <= 994 < 1000
, et(994 - 4) % 2 == 0
.Le code C complet est inclus ci-dessous, ce qui est un peu plus détaillé en raison de la gestion de la mémoire et des détails de comptage des références, mais l’idée de base est la suivante:
static int
range_contains_long(rangeobject *r, PyObject *ob)
{
int cmp1, cmp2, cmp3;
PyObject *tmp1 = NULL;
PyObject *tmp2 = NULL;
PyObject *zero = NULL;
int result = -1;
zero = PyLong_FromLong(0);
if (zero == NULL) /* MemoryError in int(0) */
goto end;
/* Check if the value can possibly be in the range. */
cmp1 = PyObject_RichCompareBool(r->step, zero, Py_GT);
if (cmp1 == -1)
goto end;
if (cmp1 == 1) { /* positive steps: start <= ob < stop */
cmp2 = PyObject_RichCompareBool(r->start, ob, Py_LE);
cmp3 = PyObject_RichCompareBool(ob, r->stop, Py_LT);
}
else { /* negative steps: stop < ob <= start */
cmp2 = PyObject_RichCompareBool(ob, r->start, Py_LE);
cmp3 = PyObject_RichCompareBool(r->stop, ob, Py_LT);
}
if (cmp2 == -1 || cmp3 == -1) /* TypeError */
goto end;
if (cmp2 == 0 || cmp3 == 0) { /* ob outside of range */
result = 0;
goto end;
}
/* Check that the stride does not invalidate ob's membership. */
tmp1 = PyNumber_Subtract(ob, r->start);
if (tmp1 == NULL)
goto end;
tmp2 = PyNumber_Remainder(tmp1, r->step);
if (tmp2 == NULL)
goto end;
/* result = ((int(ob) - start) % step) == 0 */
result = PyObject_RichCompareBool(tmp2, zero, Py_EQ);
end:
Py_XDECREF(tmp1);
Py_XDECREF(tmp2);
Py_XDECREF(zero);
return result;
}
static int
range_contains(rangeobject *r, PyObject *ob)
{
if (PyLong_CheckExact(ob) || PyBool_Check(ob))
return range_contains_long(r, ob);
return (int)_PySequence_IterSearch((PyObject*)r, ob,
PY_ITERSEARCH_CONTAINS);
}
La "viande" de l'idée est mentionnée dans la ligne :
/* result = ((int(ob) - start) % step) == 0 */
Pour finir, regardez la fonction range_contains
au bas de l'extrait de code. Si la vérification de type exacte échoue, nous n'utilisons pas l'algorithme intelligent décrit, mais retombons à la recherche d'une itération idiote de la plage à l'aide de _PySequence_IterSearch
! Vous pouvez vérifier ce comportement dans l'interpréteur (j'utilise ici la v3.5.0):
>>> x, r = 1000000000000000, range(1000000000000001)
>>> class MyInt(int):
... pass
...
>>> x_ = MyInt(x)
>>> x in r # calculates immediately :)
True
>>> x_ in r # iterates for ages.. :(
^\Quit (core dumped)
Pour ajouter à la réponse de Martijn, c’est la partie pertinente de la source (en C, l’objet de plage étant écrit en code natif):
static int
range_contains(rangeobject *r, PyObject *ob)
{
if (PyLong_CheckExact(ob) || PyBool_Check(ob))
return range_contains_long(r, ob);
return (int)_PySequence_IterSearch((PyObject*)r, ob,
PY_ITERSEARCH_CONTAINS);
}
Ainsi, pour les objets PyLong
(qui est int
en Python 3), il utilisera la fonction range_contains_long
pour déterminer le résultat. Et cette fonction vérifie essentiellement si ob
est dans la plage spécifiée (bien que cela semble un peu plus complexe en C).
S'il ne s'agit pas d'un objet int
, il retourne à une itération jusqu'à ce qu'il trouve la valeur (ou non).
Toute la logique pourrait être traduite en pseudo-Python comme ceci:
def range_contains (rangeObj, obj):
if isinstance(obj, int):
return range_contains_long(rangeObj, obj)
# default logic by iterating
return any(obj == x for x in rangeObj)
def range_contains_long (r, num):
if r.step > 0:
# positive step: r.start <= num < r.stop
cmp2 = r.start <= num
cmp3 = num < r.stop
else:
# negative step: r.start >= num > r.stop
cmp2 = num <= r.start
cmp3 = r.stop < num
# outside of the range boundaries
if not cmp2 or not cmp3:
return False
# num must be on a valid step inside the boundaries
return (num - r.start) % r.step == 0
Si vous vous demandez pourquoi cette optimisation a été ajoutée à range.__contains__
, et pourquoi n'était pas ajouté à xrange.__contains__
dans la version 2.7:
Tout d’abord, comme l’a découvert Ashwini Chaudhary, numéro 1766304 a été ouvert explicitement pour optimiser [x]range.__contains__
. Un correctif pour cela était accepté et enregistré dans la version 3.2 , mais pas en backport à la version 2.7 car "xrange s'est comporté de la sorte depuis si longtemps que je ne vois pas ce que cela nous permettrait d'acheter le correctif de cette en retard." (2,7 était presque sorti à ce moment-là.)
Pendant ce temps:
À l'origine, xrange
était un objet pas-tout-à-séquence. Comme les documents 3.1 } _, dites:
Les objets Range ont très peu de comportement: ils ne prennent en charge que l'indexation, l'itération et la fonction
len
.
Ce n'était pas tout à fait vrai. un objet xrange
supportait en fait quelques autres choses qui viennent automatiquement avec l'indexation et len
,* y compris __contains__
(via la recherche linéaire). Mais personne ne pensait que cela valait la peine de leur faire des séquences complètes à l’époque.
Ensuite, dans le cadre de la mise en œuvre du PEP { classes de base abstraites }, il était important de déterminer quels types intégrés devaient être marqués comme implémentant quel ABC, et xrange
/range
prétendait implémenter collections.Sequence
, même s'il était uniquement géré le même "très peu de comportement". Personne n’a remarqué ce problème jusqu’à numéro 9213 . Le correctif de ce problème a non seulement ajouté index
et count
à la range
de 3.2, mais il a également retravaillé le __contains__
optimisé (qui partage le même calcul avec index
et est directement utilisé par count
).** Ce changement } est également entré dans la version 3.2 et n'a pas été rétroporté en 2.x, car "c'est un correctif qui ajoute de nouvelles méthodes". (À ce stade, le statut 2.7 était déjà passé.)
Donc, il y avait deux chances pour que cette optimisation soit rétroportée à 2,7, mais elles ont toutes deux été rejetées.
* En fait, vous obtenez même une itération gratuite avec len
et l'indexation, mais les objets { dans 2.3xrange
ont un itérateur personnalisé. Ce qu’ils ont ensuite perdu dans 3.x, qui utilise le même type listiterator
que list
.
** La première version l'a réellement réimplémentée et s'est trompée - par exemple, elle vous donnerait MyIntSubclass(2) in range(5) == False
. Mais la version mise à jour du correctif de Daniel Stutzbach a restauré la majeure partie du code précédent, y compris le repli sur le générique, lent _PySequence_IterSearch
que la version antérieure à 3.2 range.__contains__
utilisait implicitement lorsque l'optimisation ne s'applique pas.
Les autres réponses l'expliquaient déjà bien, mais j'aimerais proposer une autre expérience illustrant la nature des objets de plage:
>>> r = range(5)
>>> for i in r:
print(i, 2 in r, list(r))
0 True [0, 1, 2, 3, 4]
1 True [0, 1, 2, 3, 4]
2 True [0, 1, 2, 3, 4]
3 True [0, 1, 2, 3, 4]
4 True [0, 1, 2, 3, 4]
Comme vous pouvez le constater, un objet de plage est un objet qui se souvient de sa plage et peut être utilisé plusieurs fois (même en itérant dessus), pas seulement un générateur ponctuel.
Il s’agit d’une approche paresseuse de l’évaluation et de l’optimisation supplémentaire de range
. Les valeurs dans les intervalles n’ont pas besoin d’être calculées avant une utilisation réelle, voire davantage en raison d’une optimisation supplémentaire.
En passant, votre entier n'est pas si gros, considérez sys.maxsize
sys.maxsize in range(sys.maxsize)
est assez rapide
en raison de l'optimisation - il est facile de comparer un nombre entier donné uniquement avec les valeurs min et max.
mais:
float(sys.maxsize) in range(sys.maxsize)
est assez lent .
(dans ce cas, il n'y a pas d'optimisation dans range
, donc si python reçoit un float inattendu, python comparera tous les nombres)
Vous devez être conscient des détails de la mise en œuvre mais ne pas s'y fier, car cela pourrait changer à l'avenir.
Voici implémentation similaire dans C#
. Vous pouvez voir comment Contains
est fait dans O(1) heure.
public struct Range
{
private readonly int _start;
private readonly int _stop;
private readonly int _step;
//other methods/properties omitted
public bool Contains(int number)
{
// precheck: if the number isnt in valid point, return false
// for example, if start is 5 and step is 10, then its impossible that 163 be in range at any interval
if ((_start % _step + _step) % _step != (number % _step + _step) % _step)
return false;
// v is vector: 1 means positive step, -1 means negative step
// this value makes final checking formula straightforward.
int v = Math.Abs(_step) / _step;
// since we have vector, no need to write if/else to handle both cases: negative and positive step
return number * v >= _start * v && number * v < _stop * v;
}
}
L'objet renvoyé par range()
est en réalité un objet range
. Cet objet implémente l'interface d'itérateur afin que vous puissiez parcourir ses valeurs de manière séquentielle, comme un générateur, mais il implémente également l'interface __contains__
qui est en fait ce qui est appelé lorsqu'un objet apparaît à droite de l'opérateur in
. La méthode __contains__()
renvoie une valeur booléenne indiquant si l'élément est dans l'objet ou non. Étant donné que les objets range
connaissent leurs limites et leur foulée, il est très facile de les implémenter dans O (1).