Python では関数を定義するときに引数にデフォルト値を持たせることができます。
下記の関数は、辞書に対してキーを指定して値を取り出す関数ですが、辞書にキーが含まれない場合にデフォルト値 0 を返します。
def get(dic, key, default = 0):
if key in dic:
return dic[key]
else:
return default
やってみましょう。
# 辞書を用意
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 が返されます。
ここまでは全く問題ありません。
では、次のような場合を考えてみましょう。
dict_PPAP = {"PP": ["pen", "pineapple"], "AP": ["apple", "pen"],
"PPAP": ["pen", "pineapple", "apple", "pen"]}
この辞書は、省略形(acronym)に対して、対応する原形の配列を格納した辞書です。
この辞書に対して上記と同じような関数を書いてみましょう。
def restore_acronym(dic, key, default = []):
if key in dic:
return dic[key]
else:
return default
実行してみます。
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 に対しては辞書に含まれないためデフォルト値である空配列が返されます。
思い通りの結果が返ってきました。
しかし、このように書かれた関数を使用すると、意外な結果になることがあります。
次のように、この関数からデフォルト値である空配列を取得して、いくつかの要素を追加してみましょう。
my_list = restore_acronym(dict_PPAP, "PA")
print my_list
my_list.append("pico")
my_list.append("taro")
print my_list
[]
['pico', 'taro']
この操作は何の問題も無いように思えますが、意外な結果を引き起こします。
この操作の後に、辞書に含まれないキー(例えばBANANA)を指定して、デフォルト値を取得してみます。
print "BANANA:", restore_acronym(dict_PPAP, "BANANA")
BANANA: ['pico', 'taro']
先ほどの操作の影響が、デフォルト値に残っていることがわかります。
この原因は、Python の関数のデフォルト引数が、モジュールロード時の一回だけ評価されることに起因します。
例えば、次のような関数を考えてみましょう。
from datetime import datetime
def my_log(message, timestamp = datetime.now()):
print "{timestamp}: {message}".format(timestamp = timestamp, message = message)
この関数は、ログメッセージを出力するときに、その時のタイムスタンプも同時に出力することを意図しています。
しかし、実際に実行してみると、次のようになります。
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
を使って次のように修正します。
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
を指定することで解決します。
def restore_acronym2(dic, key, default = None):
if default is None:
default = []
if key in dic:
return dic[key]
else:
return default
これで、関数呼び出しのたびに空配列のオブジェクトが生成されるようになりました。
実際、デフォルトの空配列を取得し、それに要素を追加しても、次のデフォルト呼び出しには影響しなくなります。
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!