Edited at

typedtupleという酔狂なものを作った

More than 1 year has passed since last update.


typedtupleとは

平たく言えば、voluptuousの形式で宣言的に記述可能/生成時にバリデーションを行うnamedtuple。所謂case class的な利用を意図している。


もう少し詳しく

例えばDBのrow itemを色々ハンドリングする時に、生dictでもいいんだけど添字アクセスとかだるいしコードそのもので意図を伝えられるように型化したいんだけど、そのためにclassを定義するのはtoo matchなんだよなーと思うことはないだろうか。私はあります。

また、toolzとかfn.py使ってpythonでも大分FPっぽく書けるけど、immutableなobjectが欲しいんだよなーと思うことはないだろうか。私はあります。

どちらもそれだけなら単にnamedtupleを使えば全て解決する問題ではあるが、その上で、アプリケーションコードを大量に書いているといちいち面倒になってくるのが属性の型が今なんなのかチェックする必要があるということや、例えばISO-8601な文字列を自動的にdatetimeに変換したい、ということだった。

このような事情に対して、型の構造の宣言的な記述を行うだけでnamedtupleの生成時に型チェックや変換が自動的に行われる実装を提供しようというのがtypedtupleである。


使い方

こちらに色々書いているが、typedtuple.TypedTupleをnamedtupleと同じ要領で使うかtypedtuple.schemaデコレータを使う(TypedTupleにプロパティやメソッドを追加したい場合)かして、voluptuous.Schemaを指定することで型の定義を行う。インスタンスの生成時に、指定したSchemaを利用して値の検証や自動変換が行われる。voluptuousによる検証・変換についてはそちらのドキュメントやコードを参照のこと。

from typedtuple import TypedTuple

from voluptuous import Coerce

Vector = TypedTuple('Vector', {'x': Coerce(float), 'y': Coerce(float)})

v0 = Vector(x=10, y='20.0')

# voluptuous.Coerceにより、floatに変換可能なものは自動的に変換される
print v0 # Vector(x=10.0, y=20.0)

v1 = Vector(x='x', y=20.0) # raises exception

# TypedTuple=namedtupleでありimmutableなので、属性の更新はnamedtuple._replaceをを用いて別の値を生成することになる
v2 = v0._replace(y=30.0)


注意点

如何せん要素順序が保証されないdictで構造を指定するので、実態はtupleなのに属性の順番を指定することがそのままだとできない。基本的な利用では問題にならないはずだが、順序を厳格に指定したい場合はcollections.OrderedDictを使えばよい。

from collections import OrderedDict

from typedtuple import TypedTuple
from voluptuous import Coerce

Vector = TypedTuple('Vector', OrderedDict((('x', Coerce(float)), ('y', Coerce(float))))
print Vector._fields # 必ず('x', 'y', 'z')になる(OrderedDictを使わない場合、順序は不定になる)

他の注意点としては、多分生成がかなり重い。多分とか言ってても仕方ないのでベンチマークを取る。


その他の例

from voluptuous import All, Range, Length, Optional

from typedtuple import schema

@schema({
'id': All(int, Range(min=0)), # idは必ず正の整数とか
'password': All(str, Length(min=8)), # passwordは8文字以上の文字列とか
'first_name': str,
'last_name': str,
Optional('nickname'): str, # Optionalにすると生成時省略可能
})
class User(object):
# typedtuple(というかnamedtuple)は属性を追加することはできないが、プロパティやメソッドは追加可能
@property
def full_name(self):
return '{} {}'.format(self.first_name, self.nick_name)

# raises exception
alice = User(id=-1, password='xxxxxxxx', first_name='Alice', last_name='Kakehashi', nick_name='kkhs')

# raises exception
bob = User(id=1, password='short', first_name='Bob', last_name='Kakehashi', nick_name='kkhs')

# nick_name is optional
carol = User(id=2, password='xxxxxxxx', first_name='Carol', last_name='Kakehashi')


終わり

そのうちvoluptuousについても書こうと思う。