空のリストを作成するには、list関数を使う方法と空のリストリテラル([])を使う方法が考えられます。結果は同じになる、これら2つの方法ですが、そこに違いはあるのかないのか、知っていますか?
空のリストを作成する次の2つのコードには何か違いがあるのだろうか? あるとしたら、どんな違いがあるか、考えてみよう。
どうもHPかわさきです。
ゴールデンウィークはどうでしたか? 筆者的には、推しのサッカーチームがあまり勝てなかったこともあり、お酒を飲み過ぎることもなく終わったゴールデンウィークになりました。まあ、チームが勝ってくれるとお酒が進んじゃったでしょうから、よしとしましょう。
今回はPythonのコードをズラズラと書いて、そこから何かを見つける形式の問題ではなく、2つのコードを比べる形式にしてみました。明快な答えがあるかといえば、あるようなないような。でも、上の2つのコードに差があるのかないのか。ちょっと考えてみてくださいね。
2つのコードは空のリストを作成するという結果については同じですが、Python処理系による2つのコードの解釈には違いがあります。
関数呼び出しが発生するため、わずかではありますが前者の方が実行にかかる時間が多くなる点も覚えておきましょう。
どういうことなのかは、この下で見ていきましょう。
まず、以下のコードから考えてみます。
mylist = list()
これはlist関数の呼び出しとして扱われます。このときには次のような順番で名前空間が検索されて、最初に見つかった「list」という名前に結び付いている(呼び出し可能な)オブジェクトが使われます。
ただし、上の順序はPythonにおいて名前解決が行われる際の一般的な順序を示したものであることには注意が必要です。関数(list関数のような組み込み関数やモジュールのトップレベルで定義される関数)の場合には、3のグローバル名前空間と4の組み込みの関数群から解決されるようなバイトコードが生成されます(以下で見てみましょう)。
この検索順序はLEGBと略して表現されることもあります。Lはローカル(Local)を、Eは外側(Enclosing)を、Gはグローバル(Global)を、Bは組み込み(Builtins)を意味しています。
では、次のコードはどうでしょう。
mylist = []
Pythonの処理系はこのコードを見ると、「ああ、空のリストを作りたいんだね」と判断して、すぐに空のリストを作成してくれます。このときには、上で見たlist関数呼び出しのように名前空間を順番に検索して……のようなことは行われません。
Pythonのバイトコードを見てみると、そのことがよく分かります。試してみましょう。
from dis import dis
def with_list_func():
mylist = list()
def with_brackets():
mylist = []
print('=== dis of with_list_func ===')
dis(with_list_func)
print('\n=== dis of with_brackets ===')
dis(with_brackets)
with_list_func関数は名前の通り、list関数を使って空のリストを作成するだけの関数で、with_brackets関数は角かっこ「[]」で空のリストを作成するだけの関数です。これら2つの関数を定義して、そのバイトコードをdisモジュールのdis関数で表示しています。筆者の手元のPython 3.13環境を使って、このコードを実行した結果を以下に示します。
「=== dis of with_list_func ===」の下にあるのがwith_list_func関数のバイトコードです。ポイントは「LOAD_GLOBAL」命令と「CALL」命令です。Pythonでは「list()」を関数呼び出しとして扱い、LOAD_GLOBAL命令で名前解決を行って、CALL命令で名前解決された関数を実際に呼び出しています。これに対して「=== dis of with_brackets ===」の下にあるwith_brackets関数のバイトコードではBUILD_LISTという命令だけでリストが作成されていることが分かります。
先ほど、関数についてはグローバル名前空間と組み込みの関数群から解決されるといいましたが、LOAD_GLOBAL命令はまさにこれを行うものです。なお、深くは語りませんが、なぜLOAD_GLOBALでよいのか? までをバイトコードへのコンパイルという観点で考えていくと、関数名についてもLEGBの順で名前が解決されていると捉えられるでしょう。
このように、結果は同じですが、一方は関数呼び出しの結果としてリストを作成し、もう一方はリストをそのまま作成するという違いがあるということです。
この違いは、わずかではありますが、実行速度にも影響を及ぼします。以下は上で定義した2つの関数を100万回ずつ実行し、その実行時間を計測するコードです。
from time import time
def measure(func):
st = time()
for _ in range(1_000_000): # 100万回
func()
ed = time()
return ed - st
t0 = measure(with_list_func)
t1 = measure(with_brackets)
print(f'with_list_func: {t0:.4}')
print(f'with_brackets: {t1:.4}')
これを実行した結果を以下に示します。
筆者の環境ではlist関数を使って空のリストを作成する関数を100万回呼び出すのにかかった時間は0.1371秒で、角かっこ「[]」を使って空のリストを作成する関数では0.07362秒となりました。100万回の実行で0.06秒程度の差なので、日々のPythonプログラミングでこの差を気にするかどうかといえば、気にする必要はないでしょう。とはいえ、2つのやり方で生成されるバイトコードが違うこと、実行にかかる時間が違うことを頭の片隅に留めておくと、いつか自慢できる日があるかもしれません(多分、ないです)。
今回は知っていてもあまり役には立たないけれど(役に立つのかな?)、知っているとちょっと面白いかも? というところを狙った問題にしてみました。まあ、タイプ量を考えたら、普通に角かっこ「[]」を使っちゃいますよね。そして、自分がPythonインタプリタだったら、角かっこを見た瞬間に「あ! リストだ」って判別するでしょうから、普段は角かっこを使うんじゃないかなぁ、というのが問題と答えと解説を書き終わった筆者の心境です。でも、list関数を使っても何の問題もないと思います。
というわけで、Python入門では以下の3回に分けてリストを扱っています。今回のクイズのネタとはあまり関係ないのですが、よろしかったら目を通してくださいませ。
Copyright© Digital Advantage Corp. All Rights Reserved.