matrk's blog

十中八九Python書いてる

デフォルト値にミュータブルを設定するとキャッシュされる話

 Pythonドキュメンテーション "プログラミングFAQ" にある、 "なぜオブジェクト間でデフォルト値が共有されるのですか?" について、ちょっと読むだけだと分かりづらいところがあったので、挙動を確認してみました

 ちなみにこの質問はPython2.7 / 3.4以降のドキュメンテーションでないと投稿されておらず、かつこの質問の原文へのリンクを踏むと何故か3.3の原文に飛ばされますので、URLの3.3を3とか3.4とかにすると原文読めます。

FAQの概要

 このFAQの概要は、「デフォルト値にミュータブルなオブジェクトを設定するとキャッシュされるよ」ということです。ミュータブルなオブジェクトについては、こちら(3. データモデル - Python 3.4.2 ドキュメント)にあります。簡単に取り上げると、

…値を変更できるオブジェクトのことを mutable と呼びます。 生成後に値を変更できないオブジェクトのことを immutable と呼びます。…

とあります。具体的には、組み込まれているものだとlist bytearray set dictなんかがあります。
 余談ですが、意味を重要視しているのか訳者の分担の都合なのか、原文で "mutable ~" となっている部分が和訳では「mutableな」「ミュータブルな」「変更可能な」と使い分けられていて、バーっと流し読みするとどこにミュータブルの事が書いてるのかよくわかりません。対義語のイミュータブルもそんな感じです。

挙動

>>> def foo(mutable=[]):
...   print(mutable)
...   mutable.append('bar')
...
>>> foo()
[]
>>> foo()
['bar']
>>> foo()
['bar', 'bar']
>>> foo([])
[]
>>> foo()
['bar', 'bar', 'bar']

ワオ。

どういうこったい

 デフォルト値は、関数が定義されたときに一度だけ生成されるらしいです。Pythonが参照しまくってることと組み合わせれば、うっかり書いちゃいそうなこんなケースを作る事ができます。

>>> def foo(mutable=[]):
...   mutable.append('bar')
...   return mutable
...
>>> bar = []
>>> baz = []
>>> bar.append(foo())
>>> baz.append(foo())
>>> baz.append(foo())
>>> bar
[['bar', 'bar', 'bar']]
>>> baz
[['bar', 'bar', 'bar'], ['bar', 'bar', 'bar']]

ワオ。ワーオ。
ワオ。

うさげ

 この仕様によってメモ化(FAQでは "memoizing" と紹介されていました)を実装し、キャッシュした値を返す事で関数を高速化する方法がFAQに紹介されています。是非一読を。また、キャッシュを回避する方法もFAQにあります。

 いい仕様かどうかはここでは気にしないとして、私はこの仕様を気にした事は無いです。というか知りませんでした。とりあえず把握しておく系仕様って感じがします