🦧

Pythonのリストを少しだけ深掘りしてしまった話

2024/12/03に公開

はじめに

個人開発でPythonを触っていて、リストの仕組みについて気になってしまい、調べる内に止まらなくなったのでまとめました。沼らない範囲で調べるのを辞めたので、専門の人からしたら物足りない記事かもしれないですが、生温かい目で見守ってください。
Pythonのリストは非常に便利なデータ構造であり、配列のような操作が直感的に行えます。しかし、その内部構造や仕組みを理解しているでしょうか?私は理解しきれていませんでした。この記事では様々な疑問に答えながら、リストの動きを勝手に解説していきます。

シーケンスプロトコルとは?

Pythonの「プロトコル」とは、ネットワーク通信のルールではなく、オブジェクトが持つべき「振る舞い(インターフェース)」を定義したものです。

シーケンスプロトコルの役割

リストやタプルなどの「シーケンス型」は、シーケンスプロトコルを実装しています。このプロトコルには、以下の操作が含まれます。

  • インデックスアクセス
my_list = [10, 20, 30]
print(my_list[1])  # 出力: 20

背景では、my_list.getitem(1) が呼び出されています。

  • スライス操作
print(my_list[0:2])  # 出力: [10, 20]

これは、my_list.getitem(slice(0, 2)) によって実現されています。

  • 反復処理
for item in my_list:
    print(item)

背景では、my_list.iter() によって反復が行われます。
他にも色々あるみたいなので、詳細は以下を参照。
https://docs.python.org/ja/3/c-api/sequence.html

スライス操作の仕組み

Pythonのスライスは、リストの部分集合を簡単に取得するための強力な機能です。構文は以下。

my_list[start:end:step]
  • start: 開始インデックス(省略すると先頭)
  • end: 終了インデックス(省略すると末尾、ただし未満)
  • step: ステップ幅(省略すると 1)
    もう少しコードで見てみます。
my_list = [10, 20, 30, 40, 50]

# インデックス 1 から 3 の要素を取得
print(my_list[1:4])  # 出力: [20, 30, 40]

# 逆順に取得
print(my_list[::-1])  # 出力: [50, 40, 30, 20, 10]

このスライスはどうやら、「半開区間」という数学的な区間の表現方法を採用しているみたいで、スライスが半開区間([start, end))を採用している理由は後述します。

動的配列とは?

Pythonのリストは「動的配列」として設計されています。つまり、要素を追加・削除すると、必要に応じて内部のメモリが自動で拡張・縮小されます。これは言葉の通りですね。

仕組み

  1. 初期メモリ確保: リストは最初に一定量のメモリを確保します。
my_list = []
  1. 要素追加時のメモリ管理: メモリが足りなくなると、新しいメモリを確保し、既存のデータをコピーして追加します。
my_list.append(10)
my_list.append(20)
  1. 拡張の効率化: メモリは通常2倍のサイズで拡張されるため、頻繁なコピーを防ぎます。
my_list = []

# 動的に要素を追加
for i in range(5):
    my_list.append(i)
    print(my_list)

出力例

[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4]

半開区間とは?なぜ採用されているのか?

半開区間([start, end))とは、「開始点を含むが、終了点を含まない」区間を意味します。Pythonではスライスやrange関数が半開区間を採用しています。

半開区間の利点

  1. 区間の長さを簡単に計算: [start, end) の長さは単純に end - start で求められます。
print(len(my_list[1:4]))  # 出力: 3
  1. 隣接区間を簡単に結合: 区間の「終点」が「次の区間の始点」になるため、結合がスムーズです。
first = my_list[:3]  # [10, 20, 30]
second = my_list[3:] # [40, 50]
print(first + second)  # 出力: [10, 20, 30, 40, 50]
  1. 空区間を簡単に表現: start と end が同じなら空の区間を表します。
print(my_list[2:2])  # 出力: []
  1. ループ処理が直感的: 半開区間は、ループ処理と相性が良い設計です。
for i in range(1, 4):  # インデックス 1 から 3 を処理
    print(my_list[i])

閉区間との比較も置いときます。

閉区間([start, end])との比較

閉区間[start, end]では、終了点を含むため、長さ計算や結合で特別な扱いが必要になります。これが半開区間が採用される理由です。

最後に

Pythonのリストが使いやすい理由は、その設計にあります。

  • シーケンスプロトコルにより、リストはインデックスアクセスやスライスなどの直感的な操作が可能。
  • 動的配列の内部構造が要素の追加・削除を効率化。
  • 半開区間の採用で、計算や操作の一貫性が高まり、エラーが減少。
    これらを理解すると、Pythonのリストをより効率的に活用できるようになると思いました。たかがリストされどリスト、の考えで「これってどういう仕組みで動いてるん?」の素朴な疑問を持つことで新たな視点が持てるかもしれないですね。
    より良いハッピーハッキングライフを😄

Discussion