何かこの前の続きみたいな内容ですが、 話の出所は全然別です。 そいつが言うには、Pythonでは、 あらかじめ使用するインスタンス変数(attribute)を宣言できないのが気に入らない、とのこと。
要するに、本来
obj.attr = hoge
とするところで、
obj.atr = hoge
などと簡単に間違えることができて、 しかもちっとも文句も言われないのが気に入らないと。
アクセサーを使うというのが正しい方向性なわけですけど (そもそも外部から内部構造を覗くのはオブジェクト指向的にはよろしくない)、 内部実装では当然のように直アクセスするわけだし、言いたいことは分かります。
無ければ作るべし。
def forbidNewAttributes(klass): """Forbid creation of new attributes in instances.""" original_setattr = klass.__setattr__ def strict_setattr(self, name, value, original_setattr = original_setattr, marker = object()): if getattr(self, name, marker) is not marker: original_setattr(self, name, value) else: raise AttributeError, name klass.__setattr__ = strict_setattr
この前はクラス全体を被せちゃってましたが、 クラスの継承関係が直感的でなくなるのが嫌なので、 今回は単なるメソッドの差し替えだけで。
適当に試してみます。
class Foo(object): """A demonstration.""" predefined_attribute = None def modify(self): self.predefined_attribute = True def create(self): self.new_attribute = True forbidNewAttributes(Foo) foo = Foo() try: foo.modify() print 'succeeded to modify a predefined attribute' except AttributeError: print 'failed to modify a predefined attribute' try: foo.create() print 'succeeded to create a new attribute' except AttributeError: print 'failed to create a new attribute'
結果はこうなります。
succeeded to modify a predefined attribute failed to create a new attribute
こっちはちょっと弄れば、old-style classにも適用できるはず。
この前は愛想がなかったので、今回は少しだけ説明を加えておきます。 Pythonでは、いろいろな 特殊なメソッド が用意されていて、 オブジェクトの動作を柔軟に変更することができます。 その中に __setattr__ というのがあって、 このメソッドが定義されているオブジェクトのメンバー変数に値を代入する時、 必ず呼び出されます。
__setattr__(self, name, value)
という形式で、代入されるオブジェクト、
代入する対象となるメンバー変数の名前と、
その値が渡されます。
今回の実装では、すでに存在するメンバー変数を参照して、
存在しない場合、つまり、新しいメンバー変数を登録しようとする場合、
AttributeError
という例外を投げることによって、新規登録を拒否するという方針になっています。
forbidNewAttributes
はクラス・オブジェクトを受け取って、
__setattr__
を制限付きのものに差し替えます。
original_setattr
という変数に元々存在する
__setattr__
を保存して、元の動作を不用意に削ってしまわないようにします。
こうして得られた元の
__setattr__
は、内部で動的に作成される
strict_setattr
へデフォルト引数として受け継がせます。
こうやって情報を保持する手法は、クラスを使わないで関数だけで済ませる場合、
Pythonでは常套手段です。
欠点は呼び出し側が変なパラメータを渡してくるとおかしくなってしまうところですが、
__setattr__
は言語処理系が暗黙的に呼び出すメソッドなので、
この場合には問題になりません。
実際に、あるメンバーが存在するかどうかを調べるのに、
ここでは
getattr
を使ってます。
hasattr
を使ってもよいのですが、
hasattr
はあらゆる例外を吸収してしまうという難点があるので、
ここでは安全側に倒して、
getattr
を使っています。
getattr
のこの使い方もイディオムと言ってよいものです。
Pythonでは、新しいオブジェクトを生成した場合、
既存のオブジェクトと
is
(同一性)で結ばれることはない、
という性質があります。
ただし例外もあって、数値は別々に生成した場合でも、
is
の関係になります。
この性質を利用して、new-style classで基底となる
object
タイプのインスタンスを生成し、
getattr
のデフォルトの返り値に指定しています。
そうすると、オベジェクトにすでにメンバーが存在すれば、
そのメンバーの値が返り、
決してその値がデフォルト値と
is
になることは起きません。
結果的に
hasattr
とほぼ同じ意味合いになります。
また、毎回デフォルト値となるオブジェクトを生成するのは無駄なので、 関数の定義時に一回だけ生成するよう、これも名前付き引数として定義しています。
あとは、すでに存在する場合は元の
__setattr__
を実行して、(普通は)値の書き換えが終了します。
そうでない場合は、AttributeError
で例外を起こし、新規作成をエラーとして扱うだけです。
Pythonでこうしたプログラミングがやりやすいのは、 以下のような特徴があるからです。
もちろん、他にも同じぐらい強力な言語処理系がありますが、 Pythonも十分に柔軟な言語であることが分かるでしょう。
と、今回はPythonの提灯持ち的なスタイルで書いてみました。 しかし、こうやって試してみると、 いかにあんまり知らない人にも分かるような解説記事を書くのが大変か、 身に染みてよく理解できますねえ。 書籍執筆者には頭が下がる思いです。
正統な使い方なのかは知らないのですが、 __slots__ に変数名を列挙しておくことでも制限できます。<br>>> class Foo(object): __slots__ = ['attr']<br>>> foo = Foo()<br>>> foo.attr = 0<br>>> foo.atr = 0<br>AttributeError: 'Foo' object has no attribute 'atr'
おお、確かにそうですね。ありがとうございます。<br>本来の意図はdictオブジェクトを小さいインスタンスに必ず作るのは無駄が多いので云々とリファレンスには書いてますよね。でもやりたかったことは__slots__で十分できるわけで、それでいいような気がします。