Pourquoi la plage (début, fin) n'inclut-elle pas la fin?

305
>>> range(1,11)

vous donne

[1,2,3,4,5,6,7,8,9,10]

Pourquoi pas 1-11?

Ont-ils simplement décidé de le faire comme ça au hasard ou cela a-t-il une valeur que je ne vois pas?

MetaGuru
la source
11
lire Dijkstra, ewd831
SilentGhost
11
Fondamentalement, vous choisissez un ensemble de bogues ponctuels pour un autre. Un ensemble est plus susceptible d'entraîner la fin anticipée de vos boucles, l'autre est susceptible de provoquer une exception (ou un débordement de tampon dans d'autres langues). Une fois que vous avez écrit un tas de code, vous verrez que le choix du comportement range()a beaucoup plus de sens
John La Rooy
32
Lien vers Dijkstra, ewd831: cs.utexas.edu/users/EWD/ewd08xx/EWD831.PDF
unutbu
35
@unutbu Cet article de Djikstra est souvent cité dans ce sujet mais ne fournit rien de valable ici, les gens l'utilisent juste comme un appel à l'autorité. La seule pseudo-raison pertinente qu'il donne pour la question du PO est qu'il se trouve que l'inclusion de la limite supérieure devient "contre nature" et "moche" dans le cas particulier où la séquence en vide - c'est une position entièrement subjective et facilement contestable, donc ça n'apporte pas grand chose à la table. L '«expérience» avec Mesa n'a pas beaucoup de valeur non plus sans connaître leurs contraintes particulières ou leurs méthodes d'évaluation.
Sundar
6
@andreasdr Mais même si l'argument cosmétique est valide, l'approche de Python n'introduit-elle pas un nouveau problème de lisibilité? Dans l'anglais d'usage courant, le terme "plage" implique que quelque chose varie de quelque chose à quelque chose - comme un intervalle. Que len (list (range (1,2))) renvoie 1 et len ​​(list (range (2))) renvoie 2 est quelque chose que vous devez vraiment apprendre à digérer.
armin

Réponses:

246

Parce qu'il est plus courant d'appeler range(0, 10)qui renvoie [0,1,2,3,4,5,6,7,8,9]qui contient 10 éléments ce qui équivaut len(range(0, 10)). N'oubliez pas que les programmeurs préfèrent l'indexation basée sur 0.

Considérez également l'extrait de code commun suivant:

for i in range(len(li)):
    pass

Pouvez-vous voir que si cela range()montait exactement len(li), ce serait problématique? Le programmeur aurait besoin de soustraire explicitement 1. Il suit également la tendance commune des programmeurs préférant for(int i = 0; i < 10; i++)plus for(int i = 0; i <= 9; i++).

Si vous appelez fréquemment une plage avec un début de 1, vous pouvez définir votre propre fonction:

>>> def range1(start, end):
...     return range(start, end+1)
...
>>> range1(1, 10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
moinudin
la source
48
Si tel était le raisonnement, les paramètres ne seraient-ils pas range(start, count)?
Mark Ransom
3
@shogun La valeur de départ par défaut est 0, c'est range(10)-à- dire qu'elle est équivalente à range(0, 10).
moinudin
4
Votre range1ne fonctionnera pas avec des plages qui ont une taille de pas différente de celle 1.
dimo414
6
Vous expliquez que la plage (x) doit commencer par 0 et x sera la "longueur de la plage". D'ACCORD. Mais vous n'avez pas expliqué pourquoi la plage (x, y) devrait commencer par x et se terminer par y-1. Si le programmeur veut une boucle for avec i allant de 1 à 3, il doit explicitement ajouter 1. Est-ce vraiment une question de commodité?
armin
7
for i in range(len(li)):est plutôt un contre-modèle. Il faut utiliser enumerate.
han
27

Bien qu'il y ait quelques explications algorithmiques utiles ici, je pense qu'il peut être utile d'ajouter un raisonnement simple `` dans la vraie vie '' pour expliquer pourquoi cela fonctionne de cette façon, que j'ai trouvé utile lors de l'introduction du sujet aux jeunes nouveaux arrivants:

Avec quelque chose comme «range (1,10)», la confusion peut provenir du fait de penser qu'une paire de paramètres représente le «début et la fin».

C'est en fait démarrer et "arrêter".

Maintenant, s'il s'agissait de la valeur "end", alors, vous pourriez vous attendre à ce que ce nombre soit inclus comme entrée finale dans la séquence. Mais ce n'est pas la "fin".

D'autres appellent par erreur ce paramètre "count" car si vous n'utilisez que "range (n)", alors, bien sûr, il itère "n" fois. Cette logique tombe en panne lorsque vous ajoutez le paramètre de démarrage.

Le point clé est donc de se souvenir de son nom: " stop ". Cela signifie que c'est le point auquel, une fois atteinte, l'itération s'arrêtera immédiatement. Pas après ce point.

Ainsi, alors que "start" représente en effet la première valeur à inclure, en atteignant la valeur "stop", il "casse" plutôt que de continuer à traiter "celle-là aussi" avant de s'arrêter.

Une analogie que j'ai utilisée pour expliquer cela aux enfants est que, ironiquement, il vaut mieux se comporter que les enfants! Cela ne s'arrête pas après qu'il était censé le faire - il s'arrête immédiatement sans terminer ce qu'il faisait. (Ils obtiennent ceci;))

Une autre analogie - lorsque vous conduisez une voiture, vous ne passez pas un panneau d'arrêt / de rendement / de «passage» et vous vous retrouvez avec quelque part à côté ou derrière votre voiture. Techniquement, vous ne l'avez toujours pas atteint lorsque vous vous arrêtez. Il n'est pas inclus dans les «choses que vous avez transmises pendant votre voyage».

J'espère que cela aide à expliquer à Pythonitos / Pythonitas!

dingles
la source
Cette explication est plus intuitive. Merci
Fred
L'explication des enfants est juste hilarante!
Antony Hatchkins
1
Vous essayez de mettre du rouge à lèvres sur un cochon. La distinction entre "stop" et "end" est absurde. Si je passe de 1 à 7, je n'ai pas dépassé 7. C'est simplement une faille de Python d'avoir différentes conventions pour les positions de démarrage et d'arrêt. Dans d'autres langues, y compris humaines, "de X à Y" signifie "de X à Y". En Python, "X: Y" signifie "X: Y-1". Si vous avez une réunion du 9 au 11, dites-vous aux gens que c'est du 9 au 12 ou du 8 au 11?
bzip2
24

Les gammes exclusives présentent certains avantages:

D'une part, chaque élément range(0,n)est un index valide pour les listes de longueur n.

A également range(0,n)une longueur de n, pas n+1celle d'une gamme inclusive.

sepp2k
la source
18

Il fonctionne bien en combinaison avec l'indexation de base zéro et len(). Par exemple, si vous avez 10 éléments dans une liste x, ils sont numérotés de 0 à 9. range(len(x))vous donne 0-9.

Bien sûr, les gens vous diront que c'est plus Pythonic à faire for item in xou for index, item in enumerate(x)plutôt que for i in range(len(x)).

Le découpage fonctionne de cette façon aussi: les foo[1:4]éléments 1 à 3 de foo(en gardant à l'esprit que l'élément 1 est en fait le deuxième élément en raison de l'indexation à base zéro). Par souci de cohérence, ils devraient tous deux fonctionner de la même manière.

Je pense à cela comme: "le premier numéro que vous voulez, suivi du premier numéro que vous ne voulez pas ." Si vous voulez 1-10, le premier nombre que vous ne voulez pas est 11, donc c'est range(1, 11).

Si cela devient lourd dans une application particulière, il est assez facile d'écrire une petite fonction d'aide qui ajoute 1 à l'index de fin et aux appels range().

gentil
la source
1
Entendez-vous sur le tranchage. w = 'abc'; w[:] == w[0:len(w)]; w[:-1] == w[0:len(w)-1];
kevpie
def full_range(start,stop): return range(start,stop+1) ## helper function
nobar
peut-être que l'exemple énuméré devrait être lu for index, item in enumerate(x)pour éviter toute confusion
seans
@seans Merci, fixe.
kindall
12

Il est également utile pour fractionner des plages; range(a,b)peut être divisé en range(a, x)et range(x, b), alors qu'avec une plage inclusive, vous écririez soit x-1ou x+1. Bien que vous ayez rarement besoin de fractionner des plages, vous avez tendance à fractionner des listes assez souvent, ce qui est l'une des raisons pour lesquelles le découpage d'une liste l[a:b]comprend le a-ème élément mais pas le b-ème. Le fait d' rangeavoir la même propriété le rend bien cohérent.

xuanji
la source
11

La longueur de la plage est la valeur supérieure moins la valeur inférieure.

C'est très similaire à quelque chose comme:

for (var i = 1; i < 11; i++) {
    //i goes from 1 to 10 in here
}

dans un langage de style C.

Également comme la gamme Ruby:

1...11 #this is a range from 1 to 10

Cependant, Ruby reconnaît que plusieurs fois vous voudrez inclure la valeur du terminal et propose la syntaxe alternative:

1..10 #this is also a range from 1 to 10
Skilldrick
la source
17
Gah! Je n'utilise pas Ruby, mais je peux imaginer que 1..10vs 1...10est difficile à distinguer lors de la lecture de code!
moinudin
6
@marcog - quand vous savez que les deux formes existent, vos yeux s'accordent à la différence :)
Skilldrick
11
L'opérateur de la gamme Ruby est parfaitement intuitif. La forme plus longue vous donne la séquence la plus courte. toux
Russell Borogove
4
@Russell, peut-être 1 ............ 20 devrait donner la même plage que 1..10. Maintenant, ce serait du sucre syntaxique qui vaut la peine d'être changé. ;)
kevpie
4
@Russell Le point supplémentaire serre le dernier élément hors de la plage :)
Skilldrick
5

Fondamentalement, en python, range(n)itère les ntemps, ce qui est de nature exclusive, c'est pourquoi il ne donne pas la dernière valeur lorsqu'il est imprimé, nous pouvons créer une fonction qui donne une valeur inclusive, cela signifie qu'il imprimera également la dernière valeur mentionnée dans la plage.

def main():
    for i in inclusive_range(25):
        print(i, sep=" ")


def inclusive_range(*args):
    numargs = len(args)
    if numargs == 0:
        raise TypeError("you need to write at least a value")
    elif numargs == 1:
        stop = args[0]
        start = 0
        step = 1
    elif numargs == 2:
        (start, stop) = args
        step = 1
    elif numargs == 3:
        (start, stop, step) = args
    else:
        raise TypeError("Inclusive range was expected at most 3 arguments,got {}".format(numargs))
    i = start
    while i <= stop:
        yield i
        i += step


if __name__ == "__main__":
    main()
Ashish Dixit
la source
4

Considérez le code

for i in range(10):
    print "You'll see this 10 times", i

L'idée est que vous obtenez une liste de longueur y-x, que vous pouvez (comme vous le voyez ci-dessus) parcourir.

Lisez les documents python pour la plage - ils considèrent l'itération pour la boucle comme la principale utilisation.

Robert
la source
1

Il est juste plus pratique de raisonner dans de nombreux cas.

Fondamentalement, nous pourrions penser à une plage comme un intervalle entre startet end. Si start <= end, la longueur de l'intervalle entre eux est end - start. Si lenétait réellement défini comme la longueur, vous auriez:

len(range(start, end)) == start - end

Cependant, nous comptons les entiers inclus dans la plage au lieu de mesurer la longueur de l'intervalle. Pour conserver la propriété ci-dessus vraie, nous devons inclure l'un des points de terminaison et exclure l'autre.

Ajouter le stepparamètre revient à introduire une unité de longueur. Dans ce cas, vous vous attendez

len(range(start, end, step)) == (start - end) / step

pour la longueur. Pour obtenir le nombre, vous utilisez simplement la division entière.

Arseny
la source
Ces défenses de l'incohérence de Python sont hilarantes. Si je voulais l'intervalle entre deux nombres, pourquoi utiliserais-je la soustraction pour obtenir la différence au lieu de l'intervalle? Il n'est pas cohérent d'utiliser différentes conventions d'indexation pour les positions de début et de fin. Pourquoi auriez-vous besoin d'écrire "5:22" pour obtenir les positions 5 à 21?
bzip2
Ce n'est pas Python, c'est assez courant dans tous les domaines. En C, Java, Ruby, vous l'
Arseny
Je voulais dire que c'est commun pour l'indexation, pas que les autres langues ont nécessairement le même type d'objet exact
Arseny