Python の range ってなに?

1. 簡単に言えば...

数字の集まり

例えば range(10) は 0 から 9 の自然数の集まりです。

for i in range(10):
    print(i)
... 
0
1
2
3
4
5
6
7
8
9
>>> 

range オブジェクトは以下のように 添字表記 r[i] でも参照することができます。 これの方が数字の集まり感があります。

r = range(10)
r[0]
r[1]
r[2]
r[3]
r[4]
r[5]
r[6]
r[7]
r[8]
r[9]

10 未満の自然数の集まりと考えられそうです。 Python で言う自然数は後者に該当しそうです。

range(最大値)

自然数を 1, 2, 3, … とする流儀と、0, 1, 2, 3, … とする流儀があり、 前者は数論などでよく使われ、後者は集合論、論理学などでよく使われる
自然数 - Wikipedia

2. 正確に言えば...

最小値 以上、最大値 未満 に等間隔で並んでいる数字の領域

range は英語で「領域」を意味していているので range(90, 100, 2) は、 90 以上 100 未満の領域の中にある等間隔 2 で並んでいる数字の集まりと考えると良いかもしれません。

for i in range(90, 100, 2):
    print(i)
... 
90
92
94
96
98
>>> 

もうちょっと難しい言葉を使うと、下界、上限とか言ってもいいのかもしれません。

range(下界, 上限, 間隔)

range には2つの大きな疑問があります。 まず、なぜ最小値以上で最大値未満なのでしょうか? 次に、なぜ 0 からはじまるのでしょうか?

3. 疑問1なんで最大未満なのか?

答え: 数字の範囲を綺麗に表せるから

なぜ range(1, 10) と書いたときに 10 が含まれないのでしょうか? とてもややこしいことのように思います。

range は英語で「範囲」を表します。 ここで 1 から 10 までの範囲にある整数を表現する方法について考えて見たいと思います。 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 です。

a. 1 <= i <= 10  # range(1, 10) <--- ワイはこうして欲しい
b. 1 <= i <  11  # range(1, 11) <--- Python の選択
c. 0  < i <= 10  # range(0, 10)
d. 0  < i <  11  # range(0, 11)

どの表現が一番わかりやすいでしょうか? 僕は迷うことなく a を選びます。 しかし Python の開発者たちは a にしてくれませんでした。 これはなぜでしょうか?

理由

この箇所は以下の記事を参考にして書きました。 以上、未満という選択については、Dykstra から強烈に影響を受けています。

もしいまの Python の書き方、以上、未満なら、以下の2つを綺麗に表現できます。

# 1) 個数を綺麗に表現できる。
len(range(1, 3)) == -1 + 3

# 2) 0 からはじまる空の領域も綺麗に表現できる。
list(range(0, 1)) == []

もし以上、以下にしてしまうと綺麗には表現できません。

# 1) 個数を綺麗には表現できない。
len(range(1, 3)) == -1 + 3 + 1

# 2) 0 からはじまる空の領域も綺麗に表現できない。
list(range(0,  ?)) == []

list(range(0, -1)) == []
と書けばいいのかもしれませんが、
これだと自然数ではない数 -1 がはいってきます。

(まとめ)

以上のことから b の 0 <= i < n が数字の範囲を表すときには良い表現だと言えます。 こういうのを難しい言葉で左閉右開の半開区間と言います。

4. 疑問2なんで 0 からはじまるの?

答え: メモリアドレスの参照について考えたときに色々とあたりがある。

これについては以下を参考にして書きました。

4.1. なぜ疑問に感じたのか

range が 0 からはじまるのが疑問でした。 なぜならすごく読み辛くなるからです。 例えば range(10) なら 1, 2, ... 10 として欲しいです。 Python は range(0, n) を range(n) にしました。 自分は range(1, n+1) を range(n) にして欲しかったです。

range(10) は 0 からはじめる必要はあったのでしょうか?

# Python の選択
range(n) == range(0, n)

# これでも良かったんじゃないの?と思ってしまうのです。
range(n) == range(1, n + 1)

確かにダイクストラが言うには、下よりかは上の方が綺麗なのですが...

Adhering to convention a) yields, when starting with subscript 1, the subscript range 1 <= i < N+1; starting with 0, however, gives the nicer range 0 <= i < N.
Why numbering should start at zero

でも実際プログラミングをするときに range(10) が range(1, 10 + 1) であることを 気にすることはほとんどありません。 むしろ初学者にとっては理解を妨げるものです。

While Dijkstra's article (previously referenced in a now-deleted answer) makes sense from a mathematical perspective, it isn't as relevant when it comes to programming.
Why does the indexing start with zero in 'C'? - Stackoverflow

4.2. 疑問への答え

おそらくこれは直接 Python とは関係がないように感じます。 メモリについて考えます。僕はこれから逃げてきました。 そして Python にたどり着きました。逃げたのにまた戻ってきてしまいました。

コンピュータは、内部的には 0 と 1 の数字の羅列でデータを保存しています。 データを保存する領域を メモリ と言います。 メモリには 0 と 1 を保存する小さな箱が連なっています。 その 0 と 1 を保存する各箱には アドレス という番号が振られています。 0 と 1 の数字のことを総称して ビット と言います。

(問題1)

アドレスを 0 からはじめた場合、すべてのメモリのすべての箱を参照できるようにするには 何 bit 必要ですか?

(問題2)

アドレスを 1 からはじめた場合、メモリのすべてのアドレスを参照できるようにするには 何 bit 必要ですか?

(回答)

2^n bit のメモリ空間は、n bit のメモリ空間があれば参照可能です。 例えば 8 bit, 2^3 bit のメモリ空間は、3 bit のメモリ空間があれば参照が可能です。

しかし、これを 1 からはじめてしまうと 4 bit 必要になってしまいます。 半分のメモリ空間が不要になってしまいます。

具体的にどこに問題が生じるのかは、自分は正直理解していません。 低レイヤでアドレスの変換をするコードを書くよりは、 上位側で低レイヤを意識して 0 からはじめた方が、このケースでは良いような気がします。

結論
0 から数え上げると決めたことは、 下位のシステムが解釈できるようにコードを変換することを簡単にするため そのデジタルシステムの上で走っているソフトウェアも含めた、すべてのデジタルシステムで長く浸透している。 もしそうでなければ機械とプログラマの間で、メモリの参照ごとに不必要な変換が必要になっていたでしょう。 0 から数え上げることはコンパイルを簡単にします。

Conclusion
The decision to start count at 0, has since permeated all digital systems, including the software running on them, because it makes it simpler for the code to translate to what the underlying system can interpret. If it weren't so, there would be one unnecessary translation operation between the machine and programmer, for every array access. It makes compilation easier.

コンピュータサイエンス的な背景を考えれば、 インデックスを 1 からはじめてしまうことは、 C言語などより低レイヤーに移ったときに今後の理解を妨げることになります。

このことを踏まえると Python も 低レイヤー向けの言語と合わせて 0 からはじめることが、望ましいように感じます。 実際に議論したメールがあれば欲しい...

4.3. まとめ

以上、未満であれば 0 からはじまる空の領域 range を自然数で表現できます。 また 0 からはじめた方が、 将来的なコンピュータサイエンスの理解につながりそうです。

そのため C 言語などでは n 個の数字の並びを表すときに 慣習的に次のように表現することがあります。 いま見てみると for 文を1度も回さないときには n = 0 を代入すれば良いですし、n 回繰り返すときも綺麗に表現できるので、 こうしてみるととても綺麗な表現に見えますね。

for(i=0; i<n; i++){
    //
}

こっちのがわかりやすいか...

for(i=1; i<=0; i++){
    //
}

4. 疑問3リストはなぜ 0 から始まるの?

ここまで書いて思ったけどうーんやっぱり range(10), range(1, 10) は 1..10 を表現してほしいなー笑 range(0, -1) でも問題ないでしょ。 Python の int の世界では。C 言語とかなら unsigned int とかも区別しないといけないけど。

実はあたりになる箇所があります。それはリストです。 他の言語では配列と表現されたりします。

リストの添字は他言語では先頭のアドレスからの距離になります。 諸々みんなそれに従っています。

他もそうしているからうちもそうするべきだ、というわけではないのですが、 諸々の事情を鑑み、それに寄せておいた方が何かと無難な気がします。

4.1. Rust の配列

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

4.2. Go の配列

4.3. Kotlin の List

Kotlin の List もインデックスは 0 からはじまっているそうです。 もうこの辺を鑑みてくると 0 からはじめざるを得ないか...

Kotlin だけじゃなくて Go も Rust も 0 からはじまっています。 もはや諦めるべきでしょう。

4.4. Kotlin の Range 式

-1 はありえない数字、異常な値ってことで。 Kotlin には Range 式という表記があるそうです。

fun main() {
    for (i in 1..4) print(i)
}

Range 式は rangeTo 関数 によって形成されます、 Range 式で形成されたオブジェクトは in または in! で補完される .. という形式の演算子を持っています。 Range は、すべての比較可能な型のために設計されましたが、 integral primitive な型には、それに最適化した実装がなされています。 以下、いくつか range の使い方を示した例があります:
Range expressions are formed with rangeTo functions that have the operator form .. which is complemented by in and !in. Range is defined for any comparable type, but for integral primitive types it has an optimized implementation. Here are some examples of using ranges:
Ranges - Kotlin

kotolin とかも 0..3 で range(4) を表現するしね。 でも Kotlin の表記はいいとは思えない。

まず range を表現するために特殊な表現を導入してしまっているから。 Guido が却下してるのをどこかで見かけた。 次に ああやっても書けるし、こうやっても書けるというのは、よくない。

Special cases aren't special enough to break the rules.
Python にまつわるアイデア: PEP 20

5. まとめ

range は数字の領域です。 実際 for 文の中では等差数列としての性質を得ますが、 range を等差数列と表現しているサイトをいくつか見かけました。 range は数字の領域、範囲を表していますよというところから入っていくと 理解がより深まるかなと思います。

range(下界, 上限, 間隔)

また range(n) が 0 .. n - 1 だと取っ掛かりづらいですが、 数学的な意味合いと、コンピュータサイエンス的な意味合いの両方から、 おそらくこれが最も適しているだろうと思われます。