読者です 読者をやめる 読者になる 読者になる

人生リアルタイムアタック

当面はPython学習帳

Pythonの引数のデフォルト値は一度しか評価されない

先日ハマったのでメモ。

結論

Pythonの引数のデフォルト値は一度しか評価されない。 def func(url, l=[]): … としたい場合には、代わりに

def func(url, l=None):
    if l is None:
        l = []
    …

とする。

背景

GitHub APIを叩いて全てのbranch_listを取得しようと、 以下のような関数を書いた。

def fetch_all(url, all_list=[]):
    res = urllib.request.urlopen(url)
    res_link, res_body = res.getheader('Link'), res.read().decode('utf-8')
    all_list += json.loads(res_body)

    if 'rel="next"' in res_link:
        next_url = res_link.split('; rel="next"')[0].strip('<>')
        fetch_all(next_url, all_list)

    return all_list

が、2度目の fetch_all(url) 実行時には all_listが [] でなく、1度めに実行した際の配列が格納されていることに気付いた。

Pythonチュートリアルを読んでみた

Pythonチュートリアルに以下のように記載されていた。

重要な警告: デフォルト値は一度しか評価されない。デフォルト値が可変オブジェクト、すなわちリスト、ディクショナリ、およびほとんどのクラスのインスタンスであると、このことが影響する。

Pythonチュートリアル 第2版

Pythonチュートリアル 第2版

def f(a, L=[]):
    L.append(a)
    return L

print f(1) # [1]
print f(2) # [1, 2]
print f(3) # [1, 2, 3]

と実行される。 デフォルト値を共有されたくないのであれば、以下のように書く必要がある。

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

print f(1) # [1]
print f(2) # [2]
print f(3) # [3]

冒頭の例だと以下のように書き換えると問題なく動作する

def fetch_all(url, all_list=None):
    # init
    if all_list is None:
        all_list = []

    res = urllib.request.urlopen(url)
    res_link, res_body = res.getheader('Link'), res.read().decode('utf-8')
    all_list += json.loads(res_body)

    if 'rel="next"' in res_link:
        next_url = res_link.split('; rel="next"')[0].strip('<>')
        fetch_all(next_url, all_list)

    return all_list

なんで一度しか評価されないの?

programmers.stackexchange.com

によると、 def func(item, l = something.defaultList()) のように、関数の実行結果をデフォルト引数とした際に都度実行されることを防ぐ為の目的もあるようだ。