Block Rockin’ Codes

back with another one of those block rockin' codes

Pythonのデフォルト引数にハマった。

よく考えれば当たり前だったはずなんですが、ハマってしまいました。
何かと言えば、デフォルト引数の評価です。
自戒のメモ。

デフォルト引数値の評価は一回

デフォルト引数は便利なんですが、一回しか評価されないので、
dictやlist等のミュータブル(変更可能)な値を使う場合は、
変更されることに注意しないといけない。
というのは一応常識のはずで、チュートリアルにもきちんと書いてあり、
自分もちゃんと読んだ記憶がある。

例えばこのfoo()の例。
チュートリアルにある例と本質は同じですが、
__init()__でも同じことがおこります。

# -*- coding:utf-8 -*-
class foo():
    L=''
    def __init__(self,l = []):
        l.append('a')
        self.L=l
    
if __name__ == '__main__':

    f1=foo()
    print f1.L==['a'] #True
    f2=foo()
    print f2.L==['a','a'] #True
    f3=foo()
    print f3.L==['a','a','a'] #True    

    print f1.L == f2.L == f3.L #True

で、今回ハマったのは、

# -*- coding:utf-8 -*-
import time
import random

class bar():

    t,r=0,0
    def __init__(self, t=time.time(), r=random.getrandbits(64)):
        #params={ "t":t, "r":r }
        self.t=t
        self.r=r

if __name__ == '__main__':

    b1=bar() #randomもtimeも最初しか評価されない。
    b2=bar()
    bx=bar(10,20) #途中のこういうのも関係ない
    b3=bar()
  
    print b1.t == b2.t == b3.t #True
    print b1.r == b2.r == b3.r #True

評価が一回なので、何度呼んでも時間も乱数も更新されません。
t,rに対して値が固定されます。


こういった再現性の低い値が出るメソッドは、
メソッド内で呼ぶとテストしにくくなるので、
デフォルト引数にしてリストのアンパックと合わせて
値を設定すればいいと思ってやってしまいました。

# テストしにくい(ちょっと強引な)例
def fun1(self,a):
    t=time.time()
    r=random.getrandbits(64)):
    return a+t+r

#テストできん。。
eq_(fun1(10),????)

あらかじめ渡したいデータをdictで持ってれば、
アンパックで一気にガッと渡してしまうことが出来るし、
かといって、引数は増えない。

# テストしやすい(ちょっと強引な)例
def fun2(self, a, t=int(time.time()), r=random.randint(0.1000))
    return a+t+r

param={
    'a':10,
    't':1275931019,
    'r':746 }
    
#ロジックだけはテストできる
eq_(fun2(**param),1275931775)

#使う時は普通にaだけ渡せる
fun2(10) 

という感じで出来るかと思ったんですが、
先ほどのような場合はこれでは上手く行きませんね。

この場合は、一枚別のFacadeを挟んで、そちらでやるとかの方がいいかもしれません。

インタフェース設計し直しだなぁ。。

[追記]ソースがミスってたので修正。