🐡

Pythonのジェネレータを可変長引数に渡すのはやめよう

2024/01/10に公開

結論

Pythonのジェネレータを可変長引数に渡すのはやめよう。最悪メモリ不足でプログラムがクラッシュしてしまう。

やってみよう

たとえば、下記のように、値を1ずつカウントするジェネレータを生成する。
ジェネレータを生成した時点や、next()関数を実行した時点では、メモリをほとんど消費しない。しかし、可変長引数にジェネレータを渡したところで、メモリ使用量が急増し、マシンがメモリ不足に陥ってしまう。

def count_up_to():
    """無限にカウントアップするジェネレーター"""
    count = 0
    while True:
        yield count
        count += 1


def test_star_args_func(*args):
    """可変長引数のテスト関数"""
    return


if __name__ == "__main__":
    counter = count_up_to()

    print(next(counter))  # 0
    print(next(counter))  # 1
    print(next(counter))  # 2

    test_star_args_func(*counter)  # プログラムは死ぬ

なぜそうなるのか

この現象は、可変長引数に渡された変数が常にタプルへ変換されることに起因する。

たとえば、下記のように可変長引数に値やリストを渡し、関数内で引数をprintすると、可変長引数がタプルに変換されていることがわかる。

def test_star_args_func(*args):
    """可変長引数のテスト関数"""
    print(args)
    return

test_star_args_func(1, 2, 3, 4, 5)  # (1, 2, 3, 4, 5)
test_star_args_func([1, 2, 3, 4, 5])  # ([1, 2, 3, 4, 5],)

つまり、ジェネレータを可変長引数に渡すと、ジェネレータはタプルに変換されてしまう。ジェネレータをタプルへ変換するために、プログラムはジェネレータが生成する値をすべてメモリに格納してしまう。そのため、メモリ使用量が急増してしまうのである。

とくに、最初にあげた例では、ジェネレータが生成する値は無限に増えていく。よって、ジェネレータを可変長引数に渡した時点で無限ループが発生してしまい、結果としてメモリ不足に陥ってしまう。

まとめ

Pythonのジェネレータを可変長引数に渡すのはやめよう。
最悪の場合、無限ループが発生してプログラムがクラッシュしてしまう。

参考文献・URL

Discussion