LoginSignup
59
48

More than 5 years have passed since last update.

Pythonの関数定義で引数にデフォルト値を持たせるときの注意点

Posted at

Python では関数を定義するときに引数にデフォルト値を持たせることができます。

下記の関数は、辞書に対してキーを指定して値を取り出す関数ですが、辞書にキーが含まれない場合にデフォルト値 0 を返します。

Python
def get(dic, key, default = 0):
    if key in dic:
        return dic[key]
    else:
        return default

やってみましょう。

Python
# 辞書を用意
times_in_PPAP = {"pen": 2, "pineapple": 1, "apple": 1}

print "pen:\t",    get(times_in_PPAP, "pen")
print "apple:\t",  get(times_in_PPAP, "apple")
print "banana:\t", get(times_in_PPAP, "banana")
結果
pen:    2
apple:  1
banana: 0

pen と apple は辞書に含まれるため、その値が返されます。
banana は辞書に含まれないためデフォルト値である 0 が返されます。

ここまでは全く問題ありません。
では、次のような場合を考えてみましょう。

Python
dict_PPAP = {"PP": ["pen", "pineapple"], "AP": ["apple", "pen"],
             "PPAP": ["pen", "pineapple", "apple", "pen"]}

この辞書は、省略形(acronym)に対して、対応する原形の配列を格納した辞書です。
この辞書に対して上記と同じような関数を書いてみましょう。

Python
def restore_acronym(dic, key, default = []):
    if key in dic:
        return dic[key]
    else:
        return default

実行してみます。

Python
print "PP:", restore_acronym(dict_PPAP, "PP")
print "AP:", restore_acronym(dict_PPAP, "AP")
print "PA:", restore_acronym(dict_PPAP, "PA")
結果
PP: ['pen', 'pineapple']
AP: ['apple', 'pen']
PA: []

PP と AP は辞書に含まれるため、原形の配列が返されます。
PA に対しては辞書に含まれないためデフォルト値である空配列が返されます。
思い通りの結果が返ってきました。

しかし、このように書かれた関数を使用すると、意外な結果になることがあります。
次のように、この関数からデフォルト値である空配列を取得して、いくつかの要素を追加してみましょう。

Python
my_list = restore_acronym(dict_PPAP, "PA")
print my_list

my_list.append("pico")
my_list.append("taro")
print my_list
結果
[]
['pico', 'taro']

この操作は何の問題も無いように思えますが、意外な結果を引き起こします。
この操作の後に、辞書に含まれないキー(例えばBANANA)を指定して、デフォルト値を取得してみます。

Python
print "BANANA:", restore_acronym(dict_PPAP, "BANANA")
結果
BANANA: ['pico', 'taro']

先ほどの操作の影響が、デフォルト値に残っていることがわかります。

この原因は、Python の関数のデフォルト引数が、モジュールロード時の一回だけ評価されることに起因します。

例えば、次のような関数を考えてみましょう。

Python
from datetime import datetime

def my_log(message, timestamp = datetime.now()):
    print "{timestamp}: {message}".format(timestamp = timestamp, message = message)

この関数は、ログメッセージを出力するときに、その時のタイムスタンプも同時に出力することを意図しています。
しかし、実際に実行してみると、次のようになります。

Python
from time import sleep

my_log("0 sec")
sleep(1)
my_log("1 sec")
結果
2016-12-31 23:59:59: 0 sec
2016-12-31 23:59:59: 1 sec

1秒経過しているはずなのに、同じタイムスタンプが表示されてしまいました。
上で述べたとおり、デフォルト引数は関数のロード時に一回だけしか評価されません。
したがって、デフォルト引数には関数ロード時のタイムスタンプが保存され、関数を呼び出すごとにタイムスタンプが変わるということにはなりません。

この関数を意図通りに動かすためには、None を使って次のように修正します。

Python
def my_log2(message, timestamp = None):
    if timestamp is None:
        timestamp = datetime.now()
    print "{timestamp}: {message}".format(timestamp = timestamp, message = message)

my_log2("0 sec")
sleep(1)
my_log2("1 sec")
結果
2016-12-31 23:59:59: 0 sec
2017-01-01 00:00:00: 1 sec

デフォルト引数には None を指定し、timestamp = datetime.now() を関数の内部に持ってくることで、関数の呼び出しごとに timestamp を取得しています。

これで timestamp 問題は解決しました。

それでは、PPAP の問題に戻りましょう。
こちらは、デフォルト引数に空配列を指定したため、関数のロード時に一つの空配列オブジェクトが生成され、そのオブジェクトが使い回されるため、その空配列に対する変更が保存されてしまうことが問題でした。
これもデフォルト引数に None を指定することで解決します。

Python
def restore_acronym2(dic, key, default = None):
    if default is None:
        default = []
    if key in dic:
        return dic[key]
    else:
        return default

これで、関数呼び出しのたびに空配列のオブジェクトが生成されるようになりました。
実際、デフォルトの空配列を取得し、それに要素を追加しても、次のデフォルト呼び出しには影響しなくなります。

Python
my_list2 = restore_acronym2(dict_PPAP, "PA")
print my_list2

my_list2.append("pico")
my_list2.append("taro")
print my_list2

print "BANANA:", restore_acronym2(dict_PPAP, "BANANA")
結果
[]
['pico', 'taro']
BANANA: []

Python では関数のデフォルト引数に mutable なオブジェクトを設定すると、思わぬバグを生む可能性があるので注意が必要です。

Enjoy!

59
48
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
48