Python Advent Calendar 2019 23日目の記事です。
はじめに
皆さんは、traitletsというライブラリをご存知でしょうか?
私も少し前にjupyter notebookの実装を眺めていて、存在を知りました。元々IPythonの開発から生まれて切り離されたライブラリのようです。なので、IPythonやjupyter notebookを使用されている方はお世話になっていますし、なんなら知らない内に使っていたりします。
というのも、jupyter notebookやIPythonの設定ファイルは、traitletsを使って読み込まれているからです例えばjupyter_notebook_config.py
1やipython_config.py
2を編集して
# 基本コメントアウトされています
c.Application.log_datefmt = '%Y-%m-%d %H:%M:%S'
みたいな記述を見たor編集した経験のある方もおられるかもしれません。
実はここで出てくる、謎のc
は、traitletsのConfigクラスのインスタンスとなっています。そして、c.Application.log_datefmt = ...
と書いた時、これはこの記述が書かれた設定ファイルを読み込むConfigurableクラス(実際は、それらを束ねるApplicationクラス?)が管理するApplicationクラスのlog_datefmtというメンバ変数に...
の値が割り当てられます。
実際、jupyter notebookのコアクラスとも言えるNotebookAppクラス(定義)は、jupyter_coreモジュールのJupyterAppクラス(定義)を継承していますが、このJupyterAppクラスは、traitletsのApplicationクラスを継承(ここ)しています。
本記事は、このtraitletsがどういうライブラリか、ちょっと調べてみたけど、ドキュメントが雑で結局ちゃんと理解できなかったから、とりあえず存在だけ広めておく、という目的で書きました。
そして以下は、使い方の翻訳+内容希釈した文書となります。
Traitletsの使い方
pythonは、いわゆる"型"が動的に決まるため、明示的に書かない限り、クラスの属性(メンバ変数)に、好き勝手な値を割り当てる事が可能です。このクラスの属性の型をきちんと定めて、さらに細かいチェック機能を簡単に呼び出せるようにしておこう、というのがtraitletsの提供する役割の1つだと理解しています。実際はjupyterやipython実装でも用いられている設定ファイルの読み込みのほうが、主役な機能である気もしますが...
型チェック機能
HasTraitsのサブクラスFooを次のように定義します:
from traitlets import HasTraits, Int
class Foo(HasTraits):
bar = Int()
これは、通常のクラスと同様に、Fooというクラスにbarという属性(attribute)を持たせています。ただし通常のクラス変数と違い、これはtraitと呼ばれる特殊な属性になっています。
特に、このbarはintと呼ばれるタイプのtraitになっており、名前から分かるように整数値を格納するtraitです。
実際にインスタンスを生成してみましょう:
> foo = Foo(bar=3)
> print(foo.bar)
3
> foo.bar = 6
> print(foo.bar)
6
でfooは整数値"型"のbarという属性を持っており、値を変更することも可能です。
一方で、文字列を与えてみるとどうでしょうか?
> foo = Foo(bar="3")
TraitError: The 'bar' trait of a Foo instance must be an int, but a value of '3' <class 'str'> was specified.
このような型の割り当てが間違っている旨のエラーメッセージが出るはずです。これにより、__setattr__
などを用いて、型チェックを自分で実装する必要がなくなります。
ここでInt型しか紹介していませんが、Listなどのコンテナ型含め、幾らか用意されていますし、自分で定義する事も可能です。詳しくはドキュメントを参照してください。
デフォルト値設定
traitletでは、デフォルト値を、インスタンス生成時に動的に指定できます。ちなみに先のInt
というtraitタイプでは、何も指定しないとデフォルト値として0がセットされます:
> foo = Foo()
> print(foo.bar)
0
次の例では、todayというtraitに、今日の日付を格納しています。
from traitlets import Tuple
class Foo(HasTraits):
today = Tuple(Int(), Int(), Int())
@default("today")
def default_today(self):
import datetime
today_ = datetime.datetime.today()
return (today_.year, today_.month, today_.day)
> foo = Foo()
> foo.today
(2019, 12, 22)
ちなみにコードを見れば一目瞭然ですが、todayのtraitタイプは、整数値3つから成るtupleです。
Tuple
のデフォルト値は、()
なので、デフォルト値を明示しなかったり、インスタンス生成時に値を指定したりしないと、型が違うので、割り当てエラーが発生する事に注意してください。
なお、これは次のように書くのとおそらく等価だと思いますが、ロジックの切り分け、という観点からは前者の方が明らかに読みやすいですね:
class Foo(HasTraits):
today = Tuple(Int(), Int(), Int())
def __init__(self):
import datetime
today_ = datetime.datetime.today()
self.today = (today_.year, today_.month, today_.day)
値の検証
次に紹介するのは、値割り当ての検証機能です。型チェックができるようになっても、その値が適切かどうかは分かりません。例えば、あるtrait(何かの個数を表すとしましょう)が非負整数である事が要求される場合に、Int
だけでは不十分です。
もっと言えば、この制限が、別のtraitに依存している可能性もあります。例えば、月を格納したmonthと、日を格納したdayがある場合に、monthの値によって許されるdayの範囲が変わります。こういったチェックを行うのが、validate
です。
ここでは、11月と12月だけ実装してみます。
from traitlets import validate
class Foo(HasTraits):
today = Tuple(Int(), Int(), Int())
@validate('today')
def _valid_month_day(self, proposal):
year, month, day = proposal['value']
if month not in [11,12]:
raise TraitError('invalid month')
if month == 11 and day not in range(1,31):
raise TraitError('invalid day')
elif month == 12 and day not in range(1,32):
raise TraitError('invalid day')
return proposal['value']
> foo = Foo(today=(2000,12,1))
> foo.today
(2000, 12, 1)
> foo.today = (2000,13,1)
TraitError: invalid month
> foo.today = (2000,12,31)
> foo.today = (2000,12,32)
TraitError: invalid day
なお、複数のtrait変数が相互参照している場合、1つ値を変更していくと、途中で検証エラーに引っかかる可能性があります。このような場合、全てのtraitが変更されまで検証をスキップする必要があります。これはhold_trait_notifications
スコープ内で実現できます。以下の例を見てましょう:
class Foo(HasTraits):
a, b = Int(), Int()
@validate('a')
def _valid_a(self, proposal):
if proposal['value'] * self.b <= 0:
raise TraitError("invalid a")
return proposal['value']
@validate('b')
def _valid_b(self, proposal):
if proposal['value'] * self.a <= 0:
raise TraitError("invalid b")
return proposal['value']
> foo = Foo(a=1,b=1)
> foo.a = -1
> foo.b = -1
TraitError: invalid a
> with foo.hold_trait_notifications():
> foo.a = -1
> foo.b = -1
> print(foo.a, foo.b)
-1 -1
この例では、a,bという2つのtraitが定義されていますが、それらの積は非負である事が要求されるとします。すると、両方の値を負にしても、この検証は通りますが、片方だけ変更すると、検証エラーが発生してしまいます。一方、hold_trait_notifications
内でa, b両traitの値を変更すると、このスコープ終了時に遅延検証されるので、そのような心配がなくなります。
変更通知
最後に紹介するのは、traitにobserverパターンを実装する機能です。これは、指定したtraitの値が書き換わった時に(イベント発生)、何か処理を行うことができます。
class Foo(HasTraits):
bar = Int()
@observe('bar')
def _observe_bar(self, change):
...
は、もはや完全なコードではないですが、barというtraitの値が変更された際に、_observe_barという関数が実行されます。
以上、めちゃくちゃ内容が薄いですが、初めてのプログラミング言語系の投稿、という事でこれでお許しを。また、traitlets詳しい方いたら、寂しすぎるドキュメントやexamplesを充実させてください...