python 関数のオーバーロード (キーワード引数の値編)

pythonにて(Prolog風に?)引数が特定の値になったときに、呼び出す関数を切り替えるクラスを書いてみました。

  • 値によるマッチングで、ハノイの塔などもすっきり
  • 値は 範囲や集合に含まれるかもチェックできる
  • 型チェックなどにも使える (前回書いたのよりもすっきり)

弱点として、 キーワード引数部分しかチェックしてません。

クラス本体は下のほうにあります
ライセンスはパブリックドメイン扱いでお願いします。

サンプルコード

Py2 Py3 ともに動作確認済み

ハノイの塔

すっきり!


# -*- coding: utf8 -*-
from __future__ import print_function
from kwmatch import KwMatch

if __name__=="__main__":
hanoii_main=KwMatch()

@hanoii_main.case(n=1) #1枚のとき
def _(start,goal,rest,n):
return [ (start,goal) ]

@hanoii_main.default
def _(start,goal,rest,n): # それ以外
return ( hanoii_main(start,rest,goal,n=n-1)+
[(start,goal)]+
hanoii_main(rest,goal,start,n=n-1))
# n だけ キーワード引数で指定

テストは下に

型チェック


#型チェック
class Cbase(object):pass
class C1(Cbase):pass

typecheck=KwMatch()
@typecheck.case(x=Cbase)
def _(x):
return x
@typecheck.case(x=int)
def _(x):
return str(x)

@typecheck.default #例外の型を指定
def _(x):
raise TypeError(x)

c=Cbase()
c1=C1()
assert typecheck(x=c)==c
assert typecheck(x=c1)==c1 #サブクラスも通る
assert typecheck(x=3)=="3" #int の場合 文字に変換
try:
typecheck(x="a")=="a" #それ以外の型はエラー
raise Exception("TypeCheck Reak")
except TypeError as e:
e.args[0]=="a"

値を範囲指定

  • x=slice(a,b) で、 xは a以上 b以下
  • x=set([a,b,c])で、 xは a,b,cのいずれか

#値を範囲指定
range_test=KwMatch()
@range_test.case(n=0)
def _(n):
return [0]

@range_test.case(n=slice(1,3)) # slice(x,y) で範囲指定 x<=n<=y
def _(n):
return range_test(n=n-1)+[n-1]

@range_test.case(n=set((4,5))) # in set 集合に含まれるか
def _(n):
return range_test(n=n-1)+["abcdef"[n]]

@range_test.default #それ以外
def _(n):
return range_test(n=n-1)+[n+1]

range_test(n=7)==[0,0,1,2,"e","f",7,8]

コード

ちっちゃい! コメントのほうが多い!


# -*- coding: utf8 -*-
"kwmatch.py PUBLIC DOMAIN"
class KwMatch(object):
#引数の型によって比較関数切り替え
cmps={
type:lambda x,y:isinstance(x,y),
slice:lambda x,y:y.start<=x<=y.stop,
set:lambda x,y:x in y,
}
def __init__(self):
self.cases=[] # (case,fnc) のペアが caseデコレータ適用順に格納される
self.cmps=KwMatch.cmps.copy()
self.cmp_default=lambda x,y:x==y
def _defaultfnc(self,*args,**kwargs):
raise Exception
def case(self,**kwargs): # caseデコレータ 適用の順番が重要
def _(f):
self.cases.append((kwargs,f))
return f
return _
def default(self,f): # デコレータ
self._defaultfnc=f
# cmpを差し替えれば、引数同士の大小関係などもチェック可能
#分岐条件の境界テストなども、関数の中身に触れないで 条件だけチェックできる
def cmp(self,kwargs,case):
return all( [self.cmps.get(type(v),self.cmp_default)
(kwargs[k],v)
for k,v in case.items()])
def __call__(self,*args,**kwargs):
#argsでkwargsを更新するには、先に g の引数、引数名を決定する必要あり
#ニワトリとタマゴ
for c,f in self.cases: #中身はif文をループで回してる。
if self.cmp(kwargs,c):
g=f
break
else:
g=self._defaultfnc
return g(*args,**kwargs)

ハノイの塔 テスト


#エントリーポイント
def hanoii(n):
if isinstance(n,(int)) and n>0:
return hanoii_main(1,2,3,n=n)
else:
raise Exception

assert hanoii(1) == [(1,2)]
assert hanoii(2) == [(1,3),(1,2),(3,2)]
assert hanoii(3) == [
(1,2),(1,3),(2,3),(1,2),(3,1),(3,2),(1,2)]
assert hanoii(4) == [
(1,3),(1,2),(3,2),(1,3),(2,1),(2,3),(1,3),
(1,2),
(3,2),(3,1),(2,1),(3,2),(1,3),(1,2),(3,2)]

#条件の境界テスト
assert hanoii_main.cmp({"n":1},hanoii_main.cases[0][0])
assert not all([hanoii_main.cmp({"n":i},hanoii_main.cases[0][0]) for i in [0,2,3,100]])

print ("hanoii done")