python mutable default parameters

今更おったまげた。

どういうわけだか一度もハマらなかったが、なぜハマらなかったのかが良くわからない。結構やってた気がすんだけどなぁ…?

たぶん人によってはかなり学習初期にハマるんではないのかな、と思うようなこの振る舞い:

 1 Python 3.5.3 (v3.5.3:1880cb95a742, Jan 16 2017, 16:02:32) [MSC v.1900 64 bit (AMD64)] on win32
 2 Type "help", "copyright", "credits" or "license" for more information.
 3 >>> def somefun(a=[]):
 4 ...     a.append("added")
 5 ...     print(a)
 6 ...
 7 >>> class SomeClass(object):
 8 ...     def somefun(self, a=[]):
 9 ...         a.append("ADDED")
10 ...         print(a)
11 ...
12 >>> somefun()
13 ['added']
14 >>> somefun()
15 ['added', 'added']
16 >>> somefun()
17 ['added', 'added', 'added']
18 >>> sc = SomeClass()
19 >>> sc.somefun()
20 ['ADDED']
21 >>> sc.somefun()
22 ['ADDED', 'ADDED']
23 >>> sc.somefun()
24 ['ADDED', 'ADDED', 'ADDED']

ひじょーにぶったまげる振る舞いで、そして見ての通り非常に「当たり前に見えるコーディング」だから間違いなく有名な振る舞いだろうと思う。実際 Goole 検索窓で「python mutable」と打ち込むだけで、皆がこの振る舞いを調べようとしていることがわかる。のに、今の今まで気付かなかった。

確かに普通のコーディングでは「参照渡しであることをあてにして、パラメータの中身を書き換える」というような関数やメソッドはあまり書かないで済む。普通は return で返せばいいのだから。

ただ実際どうしても mutable なパラメータを変更する必要があるケースはこれは確かにあって、「何らかの事情により class に出来ないモジュール関数」が再帰するケースではそうするより仕方がない。例えばこんな:

1 def recurse_fun(k, _dejav):
2     if k in _dejav:
3         return
4     _dejav.add(k)
5     if do_some():
6         return recurse_fun(k, _dejav)

デフォルト引数を持たない場合のスコープは明確、というかプログラマ自身がコントロール出来るので問題はないが、デフォルト引数にしたいということは、「利用者からみえるが省略を許したい」かもしくは「隠したい」場合なわけで、この例の「_dejav」は「隠したい」の典型で、そう願ったら最後、地獄へ猫まっしぐら、というわけだ。

先の「somefun」は要するに以下とほぼ等価:

1 _SOMEFUN_DEFAULT_A = []
2 
3 def somefun(a=_SOMEFUN_DEFAULT_A):
4     a.append("added")
5     print(a)

結論は2つ:

  • Visual Basic や Fortran じゃあるまいし、参照渡しなノリは避けた方がええぞ
  • アタシが現実に必要としている「_dejav」のように不可欠な場合はこうすれば解決出来る:
     1 >>> def somefun(**kwargs):
     2 ...     a = kwargs.get("a", [])
     3 ...     a.append("added")
     4 ...     print(a)
     5 ...
     6 >>> class SomeClass(object):
     7 ...     def somefun(self, **kwargs):
     8 ...         a = kwargs.get("a", [])
     9 ...         a.append("ADDED")
    10 ...         print(a)
    11 ...
    12 >>> somefun()
    13 ['added']
    14 >>> somefun()
    15 ['added']
    16 >>> somefun(a=["zzz"])
    17 ['zzz', 'added']
    18 >>> mylist = []
    19 >>> somefun(a=mylist)
    20 ['added']
    21 >>> somefun(a=mylist)
    22 ['added', 'added']
    23 >>> mylist
    24 ['added', 'added']
    25 >>>
    26 >>> sc = SomeClass()
    27 >>> sc.somefun()
    28 ['ADDED']
    29 >>> sc.somefun()
    30 ['ADDED']
    31 >>> sc.somefun(a=["zzz"])
    32 ['zzz', 'ADDED']
    33 >>> mylist = []
    34 >>> sc.somefun(a=mylist)
    35 ['ADDED']
    36 >>> sc.somefun(a=mylist)
    37 ['ADDED', 'ADDED']
    38 >>> mylist
    39 ['ADDED', 'ADDED']
    

    実際この例ではユーザは必ず「a=」と明示しなければならなくなったわけで、これが意図するものでないならご愁傷様。アプローチを変えた方がええと思う。アタシの「_dejav」的な用途にはこれは却ってありがたい。利用者には見せたくないパラメータなんだもの。

2017-11-15 追記:
失礼、これでもいい:

1 def hoge(a=None):
2     if a is None:
3         a = []
4     a.append("ADDED")

これであればクライアントコードから a は隠れない。