Pourquoi «1000000000000000 à portée (1000000000000001)» est-il si rapide en Python 3?

2116

Je crois comprendre que la range()fonction, qui est en fait un type d'objet en Python 3 , génère son contenu à la volée, semblable à un générateur.

Cela étant le cas, je m'attendais à ce que la ligne suivante prenne un temps excessif, car pour déterminer si 1 quadrillion est 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 que j'ajoute, le calcul prend plus ou moins le même temps (essentiellement instantané).

J'ai également 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 si agréable !!

def my_crappy_range(N):
    i = 0
    while i < N:
        yield i
        i += 1
    return

Que fait l' range()objet sous le capot qui le rend si rapide?


La réponse de Martijn Pieters a été choisie pour son exhaustivité, mais voir également la première réponse d'Abarnert pour une bonne discussion de ce que cela signifie pour rangeêtre une séquence à part entière dans Python 3, et quelques informations / avertissements concernant l'incohérence potentielle pour __contains__l'optimisation des fonctions dans les implémentations Python . L'autre réponse d'abarnert va plus en détail et fournit des liens pour ceux qui s'intéressent à l'histoire derrière l'optimisation dans Python 3 (et le manque d'optimisation de xrangedans Python 2). Les réponses par poke et par wim fournissent le code source C pertinent et des explications pour ceux qui sont intéressés.

Rick soutient Monica
la source
70
Notez que ce n'est le cas que si l'élément que nous vérifions est un type boolou long, avec d'autres types d'objets, cela deviendra fou. Essayez avec:100000000000000.0 in range(1000000000000001)
Ashwini Chaudhary
10
Qui vous a dit que rangec'était un générateur?
abarnert
7
@abarnert Je pense que la modification que j'ai faite a laissé la confusion intacte.
Rick soutient Monica
5
@AshwiniChaudhary n'est-ce pas Python2 xrangeidentique à Python3range ?
Superbe
28
Les xrange()objets @Superbest n'ont pas de __contains__méthode, donc la vérification des éléments doit parcourir tous les éléments. De plus , il y a peu d' autres changements dans range(), comme il prend en charge le découpage (qui retourne à nouveau un rangeobjet) et maintenant aussi countet indexméthodes pour le rendre compatible avec collections.SequenceABC.
Ashwini Chaudhary

Réponses:

2171

L' range()objet Python 3 ne produit pas de nombres immédiatement; c'est un objet de séquence intelligent qui produit des nombres à la demande . Il ne contient que vos valeurs de début, d'arrêt et de pas, puis lorsque vous parcourez l'objet, l'entier suivant est calculé à chaque itération.

L'objet implémente également le object.__contains__crochet et calcule si votre numéro fait partie de sa plage. Le calcul est une opération à temps constant (presque) * . Il n'est jamais nécessaire de parcourir tous les entiers possibles de la plage.

De la range()documentation de l' objet :

L'avantage du rangeécrasez régulièrement listou tupleest qu'un objet de gamme aura toujours la même (petite) quantité de mémoire, peu importe la taille de la plage qu'elle représente (car il ne stocke que les start, stopet les stepvaleurs, le calcul des éléments individuels et les sous - domaines comme requis).

Donc, au minimum, votre range()objet 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, step = stop, start, -step
        else:
            lo, hi = start, stop
        self.length = 0 if lo > hi else ((hi - lo - 1) // 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 un réel range()(tels que les méthodes .index()ou .count(), le hachage, les tests d'égalité ou le découpage), mais devraient vous donner une idée.

J'ai également simplifié l' __contains__implémentation pour me concentrer uniquement sur les tests entiers; si vous donnez à un range()objet réel une valeur non entière (y compris les sous-classes de int), un balayage lent est lancé pour voir s'il existe une correspondance, tout comme si vous utilisez un test de confinement par rapport à une liste de toutes les valeurs contenues. Cela a été fait pour continuer à prendre en charge d'autres types numériques qui se trouvent juste à prendre en charge les tests d'égalité avec des entiers, mais qui ne devraient pas également prendre en charge l'arithmétique des entiers. Voir le problème Python d' origine qui implémentait le test de confinement.


* Temps presque constant car les entiers Python sont illimités et donc les opérations mathématiques augmentent également dans le temps à mesure que N croît, ce qui en fait une opération O (log N). Comme tout est exécuté dans du code C optimisé et que Python stocke les valeurs entières dans des morceaux de 30 bits, vous manquerez de mémoire avant de voir un impact sur les performances en raison de la taille des entiers impliqués ici.

Martijn Pieters
la source
58
Fait amusant: parce que vous avez une implémentation fonctionnelle de __getitem__et __len__, l' __iter__implémentation est en fait inutile.
Lucretiel
2
@Lucretiel: Dans Python 2.3 , une spéciale a xrangeiteratorété ajoutée spécifiquement parce que ce n'était pas assez rapide. Et puis quelque part dans 3.x (je ne sais pas si c'était 3.0 ou 3.2), il a été lancé et ils utilisent le même listiteratortype que celui listutilisé.
abarnert
1
Je définirais le constructeur comme def __init__(self, *start_stop_step)et l'analyserais à partir de là; la façon dont les arguments sont étiquetés maintenant est maintenant un peu déroutante. Néanmoins, +1; vous avez toujours définitivement expliqué le comportement.
Cody Piersall
1
@CodyPiersall: Malheureusement, c'est la signature de l'initialiseur de la vraie classe. rangeest plus ancienne que *args(et encore moins l' argclinicAPI qui permet aux fonctions C-API d'avoir des signatures Python complètes). Quelques autres anciennes fonctions (et quelques fonctions plus récentes, comme xrange, sliceet itertools.islice, pour des raisons de cohérence) fonctionnent de la même manière, mais pour la plupart, Guido et le reste des développeurs principaux semblent d'accord avec vous. Les docs 2.0+ décrivent même rangeet amis comme s'il s'agissait de surcharges de style C ++ plutôt que de montrer la signature réelle déroutante.
abarnert
2
@CodyPiersall: En fait, voici une citation de Guido lors de la argclinicdiscussion, lorsque Nick Coghlan a trouvé un moyen de permettre de définir rangesans ambiguïté: "Veuillez ne pas faciliter la copie de ma pire décision de conception." Donc, je suis presque sûr qu'il convient que cela rangeprête à confusion comme écrit.
abarnert
845

Le malentendu fondamental ici est de penser que rangec'est un générateur. Ce n'est pas. En fait, ce n'est pas une sorte d'itérateur.

Vous pouvez le dire assez facilement:

>>> a = range(5)
>>> print(list(a))
[0, 1, 2, 3, 4]
>>> print(list(a))
[0, 1, 2, 3, 4]

S'il s'agissait d'un générateur, l'itérer une fois l'épuiserait:

>>> b = my_crappy_range(5)
>>> print(list(b))
[0, 1, 2, 3, 4]
>>> print(list(b))
[]

En rangeréalité, c'est une séquence, tout comme une liste. Vous pouvez même tester ceci:

>>> import collections.abc
>>> isinstance(a, collections.abc.Sequence)
True

Cela signifie qu'il doit suivre toutes les règles d'une séquence:

>>> a[3]         # indexable
3
>>> len(a)       # sized
5
>>> 3 in a       # membership
True
>>> reversed(a)  # reversible
<range_iterator at 0x101cd2360>
>>> a.index(3)   # implements 'index'
3
>>> a.count(3)   # implements 'count'
1

La différence entre a rangeet a listest que a rangeest une séquence paresseuse ou dynamique ; il ne se rappelle pas toutes ses valeurs, il vient se souvient de son start, stopet step, et crée les valeurs à la demande sur __getitem__.

(Comme une note de côté, si vous print(iter(a)), vous remarquerez que les rangeutilisations du même listiteratortype que list. Comment ça marche? A listiteratorne pas utiliser quoi que ce soit de spécial , listsauf pour le fait qu'il fournit une implémentation C __getitem__, il fonctionne très bien pour rangeaussi.)


Maintenant, rien ne dit que le temps Sequence.__contains__doit être constant - en fait, pour des exemples évidents de séquences comme list, ce n'est pas le cas. Mais rien ne dit que cela ne peut pas être. Et il est plus facile à implémenter range.__contains__de simplement le vérifier mathématiquement ( (val - start) % step, mais avec une complexité supplémentaire pour gérer les étapes négatives) que de générer et de tester réellement toutes les valeurs, alors pourquoi ne le ferait-il pas mieux?

Mais il ne semble pas y avoir quoi que ce soit dans la langue qui garantisse que cela se produira. Comme le souligne Ashwini Chaudhari, si vous lui donnez une valeur non intégrale, au lieu de la convertir en entier et de faire le test mathématique, cela reviendra à itérer toutes les valeurs et à les comparer une par une. Et juste parce que les versions CPython 3.2+ et PyPy 3.x contiennent cette optimisation, et c'est une bonne idée évidente et facile à faire, il n'y a aucune raison pour qu'IronPython ou NewKickAssPython 3.x ne puissent pas le laisser de côté. (Et en fait, CPython 3.0-3.1 ne l'a pas inclus.)


Si en rangefait, c'était un générateur, my_crappy_rangealors ça n'aurait pas de sens de tester de __contains__cette façon, ou du moins la façon dont ça aurait du sens ne serait pas évidente. Si vous avez déjà répété les 3 premières valeurs, est-ce 1toujours inle générateur? Le test doit- 1il le faire itérer et consommer toutes les valeurs jusqu'à 1(ou jusqu'à la première valeur >= 1)?

abarnert
la source
10
C'est une chose assez importante pour être clair. Je suppose que les différences entre Python 2 et 3 peuvent avoir conduit à ma confusion sur ce point. Dans tous les cas, j'aurais dû réaliser car rangeest répertorié (avec listet tuple) comme type de séquence .
Rick soutient Monica
4
@RickTeachey: En fait, dans 2.6+ (je pense; peut-être 2.5+), xrangec'est aussi une séquence. Voir 2.7 documents . En fait, c'était toujours une quasi-séquence.
abarnert
5
@RickTeachey: En fait, j'avais tort; en 2.6-2.7 (et 3.0-3.1), il prétend être une séquence, mais ce n'est encore qu'une quasi-séquence. Voir mon autre réponse.
abarnert
2
Ce n'est pas un itérateur, c'est une séquence (Iterable en termes de Java, IEnumerable de C #) - quelque chose avec une .__iter__()méthode qui retournera un itérateur. Il ne peut à son tour être utilisé qu'une seule fois.
Smit Johnth
4
@ThomasAhle: Parce que rangene vérifie pas les types quand ce n'est pas un entier, car il est toujours possible qu'un type ait un __eq__qui est compatible avec int. Bien sûr, strcela ne fonctionnera évidemment pas, mais ils ne voulaient pas ralentir les choses en vérifiant explicitement tous les types qui ne peuvent pas y être (et après tout, une strsous - classe pourrait remplacer __eq__et être contenue dans le range).
ShadowRanger
377

Utilisez la source , Luke!

En CPython, range(...).__contains__(un wrapper 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 plage . Pour expliquer la logique utilisée:

  1. Vérifiez que le nombre est compris entre startet stop, et
  2. Vérifiez que la valeur de pas ne dépasse pas notre nombre.

Par exemple, 994c'est range(4, 1000, 2)parce que:

  1. 4 <= 994 < 1000, et
  2. (994 - 4) % 2 == 0.

Le code C complet est inclus ci-dessous, ce qui est un peu plus détaillé à cause de la gestion de la mémoire et des détails de comptage des références, mais l'idée de base est là:

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 terminer, regardez la range_containsfonction en bas de l'extrait de code. Si la vérification de type exacte échoue, nous n'utilisons pas l'algorithme intelligent décrit, mais nous retombons à une recherche d'itération stupide de la plage en utilisant _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)
wim
la source
144

Pour ajouter à la réponse de Martijn, ceci est la partie pertinente de la source (en C, car l'objet range est é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);
}

Donc, pour les PyLongobjets (qui est inten Python 3), il utilisera la range_contains_longfonction pour déterminer le résultat. Et cette fonction vérifie essentiellement si se obtrouve dans la plage spécifiée (bien qu'elle semble un peu plus complexe en C).

S'il ne s'agit pas d'un intobjet, il revient à l'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
poussée
la source
11
@ChrisWesseling: Je pense que ce sont des informations suffisamment différentes (et suffisantes) que la modification de la réponse de Martijn n'aurait pas été appropriée ici. C'est un appel au jugement, mais les gens préfèrent généralement ne pas apporter de changements radicaux aux réponses des autres.
abarnert
105

Si vous vous demandez pourquoi cette optimisation a été ajoutée range.__contains__et pourquoi elle n'a pas été ajoutée xrange.__contains__en 2.7:

Tout d'abord, comme Ashwini Chaudhary l'a découvert, le problème 1766304 a été ouvert explicitement pour être optimisé [x]range.__contains__. Un correctif a été accepté et archivé pour la version 3.2 , mais il n'a pas été rétroporté vers la version 2.7 car "xrange s'est comporté comme ça depuis si longtemps que je ne vois pas ce qu'il nous achète pour valider le correctif si tard." (2.7 était presque sorti à ce moment-là.)

Pendant ce temps:

À l'origine, xrangeétait un objet pas tout à fait séquence. Comme le disent les documents 3.1 :

Les objets de plage ont très peu de comportement: ils ne prennent en charge que l'indexation, l'itération et la lenfonction.

Ce n'était pas tout à fait vrai; un xrangeobjet a en fait pris en charge quelques autres choses qui viennent automatiquement avec l'indexation et len, * y compris __contains__(via la recherche linéaire). Mais personne ne pensait qu'il valait la peine d'en faire des séquences complètes à l'époque.

Ensuite, dans le cadre de la mise en œuvre du PEP des classes de base abstraites , il était important de déterminer quels types intégrés devraient être marqués comme implémentant quels ABC et xrange/ ou rangeprétendaient implémenter collections.Sequence, même s'il ne traitait toujours que le même "très petit comportement". Personne n'a remarqué ce problème jusqu'au numéro 9213 . Le patch pour cette question non seulement ajouté indexet countà 3,2 de range, il a également retravaillé l'optimisation __contains__(qui partage le même calcul avec index, et est directement utilisé par count). ** Cette modification est également entrée dans la version 3.2 et n'a pas été rétroportée vers 2.x, car "c'est un correctif qui ajoute de nouvelles méthodes". (À ce stade, 2.7 avait déjà dépassé le statut rc.)

Il y avait donc deux chances d'obtenir cette optimisation rétroportée à 2,7, mais elles ont toutes deux été rejetées.


* En fait, vous obtenez même l'itération gratuitement avec l'indexation seule, mais dans 2.3 les xrange objets ont un itérateur personnalisé.

** La première version l'a en fait réimplémentée et s'est trompée de détails — par exemple, elle vous donnerait MyIntSubclass(2) in range(5) == False. Mais la version mise à jour de Daniel Stutzbach du correctif a restauré la plupart du code précédent, y compris le retour au générique, lent _PySequence_IterSearchque la version antérieure à 3.2 range.__contains__utilisait implicitement lorsque l'optimisation ne s'appliquait pas.

abarnert
la source
4
D'après les commentaires ici: améliorezxrange.__contains__ , il semble qu'ils ne l'ont pas rétroporté sur Python 2 juste pour laisser un élément de surprise aux utilisateurs et il était trop tard o_O. Le patchcount et a été ajouté plus tard. Fichier à ce moment: hg.python.org/cpython/file/d599a3f2e72d/Objects/rangeobject.cindex
Ashwini Chaudhary
12
J'ai une sinistre suspicion que certains développeurs de base de python sont partisans de "l'amour dur" pour python 2.x parce qu'ils veulent encourager les gens à passer au python3 bien supérieur :)
wim
4
Je parie également que c'est un énorme fardeau d'avoir à ajouter de nouvelles fonctionnalités aux anciennes versions. Imaginez que vous alliez chez Oracle et disiez: "Écoutez, je suis sur Java 1.4 et je mérite des expressions lambda! Rétroportez-les pour rien."
Rob Grant
2
@RickTeachey ouais c'est juste un exemple. Si je disais 1.7, cela s'appliquerait toujours. C'est une différence quantitative et non qualitative. Fondamentalement, les développeurs (non rémunérés) ne peuvent pas toujours créer de nouvelles choses intéressantes dans 3.x et les rétroporter vers 2.x pour ceux qui ne veulent pas mettre à niveau. C'est un fardeau énorme et ridicule. Pensez-vous qu'il y a encore quelque chose qui ne va pas dans mon raisonnement?
Rob Grant
3
@RickTeachey: 2,7 se situait entre 3,1 et 3,2, pas autour de 3,3. Et cela signifie que 2.7 était en rc lorsque les dernières modifications apportées à 3.2 sont entrées, ce qui rend les commentaires de bogue plus faciles à comprendre. Quoi qu'il en soit, je pense qu'ils ont fait quelques erreurs rétrospectivement (surtout en supposant que les gens migreraient via 2to3au lieu de via le code à double version avec l'aide de bibliothèques comme six, c'est pourquoi nous avons obtenu des choses comme dict.viewkeysça que personne n'utilisera jamais), et il y avait quelques changements qui sont venus trop tard dans la 3.2, mais la plupart du temps la 2.7 était une version "2.x 2. jamais" assez impressionnante.
abarnert
47

Les autres réponses l'ont déjà bien expliqué, mais je voudrais proposer une autre expérience illustrant la nature des objets de la gamme:

>>> 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 voir, 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.

Stefan Pochmann
la source
27

Il s'agit d'une approche paresseuse de l'évaluation et d'une optimisation supplémentaire derange . Les valeurs dans les plages n'ont pas besoin d'être calculées avant une utilisation réelle, ou même davantage en raison d'une optimisation supplémentaire.

Soit dit en passant, votre entier n'est pas si grand, considérez sys.maxsize

sys.maxsize in range(sys.maxsize) est assez rapide

en raison de l'optimisation - il est facile de comparer un entier donné uniquement avec le min et le max de la plage.

mais:

Decimal(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 Decimal inattendu, python comparera tous les nombres)

Vous devez être au courant d'un détail d'implémentation, mais vous ne devez pas vous y fier, car cela pourrait changer à l'avenir.

Sławomir Lenart
la source
4
Faites attention aux grands nombres flottants. Sur la plupart des machines, float(sys.maxsize) != sys.maxsize)cependant sys.maxsize-float(sys.maxsize) == 0.
holdenweb
18

TL; DR

L'objet retourné par range()est en fait unrange objet. Cet objet implémente l'interface de l'itérateur afin que vous puissiez parcourir ses valeurs séquentiellement, tout comme un générateur, une liste ou un tuple.

Mais il implémente également l' __contains__interface qui est en fait ce qui est appelé lorsqu'un objet apparaît sur le côté droit de l' inopérateur. La __contains__()méthode renvoie un boolélément indiquant si l'élément du côté gauche de inl'objet se trouve ou non dans l'objet. Puisque les rangeobjets connaissent leurs limites et leur foulée, c'est très facile à implémenter dans O (1).

RBF06
la source
0
  1. En raison de l'optimisation, il est très facile de comparer des entiers donnés uniquement avec la plage min et max.
  2. La raison pour laquelle la fonction range () est si rapide en Python3 est que nous utilisons ici un raisonnement mathématique pour les limites, plutôt qu'une itération directe de l'objet range.
  3. Donc, pour expliquer la logique ici:
    • Vérifiez si le nombre se situe entre le début et l'arrêt.
    • Vérifiez si la valeur de précision de pas ne dépasse pas notre nombre.
  4. Prenons un exemple, 997 est dans la plage (4, 1000, 3) parce que:

    4 <= 997 < 1000, and (997 - 4) % 3 == 0.

Naruto
la source
1
Pouvez-vous partager la source pour cela? Même si cela semble légitime, il serait bon de soutenir ces affirmations par du code réel
Nico Haase
Je pense que c'est un exemple de cela pourrait être mis en œuvre. Pas la façon exacte dont il est mis en œuvre. Bien qu'aucune référence ne soit fournie, c'est un bon indice suffisamment bon pour comprendre pourquoi la vérification de l'inclusion pour la plage peut être beaucoup plus rapide que la liste ou le tuple
Mohammed Shareef C
0

Essayez x-1 in (i for i in range(x))de grandes xvaleurs, qui utilisent une compréhension du générateur pour éviter d'appeler l' range.__contains__optimisation.

benjimin
la source