Python の range ってなに?

1. 簡単に言えば...

数字の集まり
for i in range(10):
    print(i)
... 
0
1
2
3
4
5
6
7
8
9
>>> 

2. 正確に言えば...

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

もうちょっと難しい言葉を使うと 下界、上限とか言ってもいいのかもしれません。
区間 (数学) - Wikipedia

range(下界,   上限,   間隔  )

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

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

3. 2つの大きな疑問

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

Dykstra から強烈に影響を受けています。 range(10) と書いたときに 10 個の数字の並びという意味です。

この記事は以下の3つの記事を参考にして書きました。

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 にしてくれませんでした。 これはなぜでしょうか? つぎの2つの問題について考えてみます。

(問題1) 空の「範囲」を表現してください。

a は -1 という自然数から離れる数でも使わないことには空の「範囲」を表現できません。 つまり a の表現が消えてしまいました。

a. -1 <= i <= 0  # range(-1, 0)
b.  0 <= i <  0  # range( 0, 0)
c.  0 <  i <= 0  # range( 0, 0)
d.  0 <  i <  1  # range( 0, 1)

最小値以上、最大値以下という a ではダメであることがわかりました。 難しい言葉で言えば閉区間では空の「範囲」は表せないことがわかりました。

では c, d の「より大きい」という表現はどうでしょうか?

(問題2) 0 から 9 までの「範囲」にある整数を表現してください。

0 は自然数に含まれる場合もあれば、含まれない場合もあります。 おやおや、 c, d は、自然数ではない -1 が登場してしまいました。 ここで c, d の表現が消えてしまいました。

a.  0 <= i <=  9
b.  0 <= i <  10
c. -1 <  i <=  9
d. -1 <  i <  10

(まとめ)

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

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

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

3.2.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

3.2.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 からはじめることが、望ましいように感じます。 実際に議論したメールがあれば欲しい...

3.3. まとめ

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

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

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

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

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

4. リストがなぜ 0 からはじまるか

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

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

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

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

4.1. Kotlin の List

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

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

4.2. Rust の配列

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

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

配列の要素にアクセスする - The Rust Programming Language

4.3. Go の配列

5. 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 だと取っ掛かりづらいですが、 数学的な意味合いと、コンピュータサイエンス的な意味合いの両方から、 おそらくこれが最も適しているだろうと思われます。