今更おったまげた。
どういうわけだか一度もハマらなかったが、なぜハマらなかったのかが良くわからない。結構やってた気がすんだけどなぁ…?
たぶん人によってはかなり学習初期にハマるんではないのかな、と思うようなこの振る舞い:
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
は隠れない。