最適化の前に測定しましょう。timeitとcProfile、GILが本当に妨げているもの、ジェネレーターの倹約さ、そして文字列とスライスに潜むメモリの落とし穴です。
このレッスンを Kodokon で開く推測は決してせず、測りましょう。timeitは信頼できるマイクロベンチマークを切り出します。ガベージコレクターを無効にし、実行を繰り返してノイズをならすのです。cProfileはプログラム全体の呼び出しを地図にします - cumtime(サブ呼び出しを含む累積時間)で並べ替えて、本当の犯人を特定しましょう。あらゆるパフォーマンス作業の黄金律はこうです。遅い行は、あなたが疑っている行であることはまずありません。
python -m timeit "sum(x * x for x in range(1000))"
python -m timeit -s "s = 'abc' * 100" "s.upper()"
python -m cProfile -s cumtime -m timeit "min(range(50))"GIL(Global Interpreter Lock)は、一つのプロセスの中で同時にPythonのバイトコードを実行するスレッドは一つだけであることを保証します。その帰結として、スレッドは純粋な計算を速くしません。とはいえI/Oには依然として最適です。ネットワークやディスクの読み込みのあいだ、そしてNumPyのような多くのCライブラリによって、GILは解放されるからです。複数のコアを計算で埋め尽くすには、multiprocessingかconcurrent.futures.ProcessPoolExecutorを経由しましょう。Python 3.13にはGILのない実験的な「フリースレッド」版がありますが、本番ではまだ稀です。
import sys
squares_list = [x * x for x in range(1_000_000)]
squares_gen = (x * x for x in range(1_000_000))
print(sys.getsizeof(squares_list), "bytes")
print(sys.getsizeof(squares_gen), "bytes")
print(sum(squares_gen))ジェネレーターは、列の長さがどうであれ数百バイトしかありません。保存するのは自分の実行状態だけで、各値は求められたときに生み出します。パイプラインとしてつなげれば - sum(x * x for x in data) - 中間のリストを一度も実体化せずにストリームを処理できます。承知しておくべきトレードオフもあります。ジェネレーターは使い切りです。いったん尽きたら、それ以上は何も生み出さず、しかも黙ったままです - 二度目の反復が「空」に見える、というバグの典型的な原因です。
import timeit
def concat(n: int) -> str:
out = ""
for i in range(n):
out += str(i)
return out
def join(n: int) -> str:
return "".join(str(i) for i in range(n))
t1 = timeit.timeit(lambda: concat(20_000), number=10)
t2 = timeit.timeit(lambda: join(20_000), number=10)
print(f"concat: {t1:.3f} s / join: {t2:.3f} s")sys.getsizeofは、同等のリストに比べてごく小さいままなのですか?''.join(parts)は、ループの中で繰り返されるout += partに勝るのですか?