2006-12-12

_ Pythonで新しいメンバーの登録を禁止してみる

何かこの前の続きみたいな内容ですが、 話の出所は全然別です。 そいつが言うには、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の提灯持ち的なスタイルで書いてみました。 しかし、こうやって試してみると、 いかにあんまり知らない人にも分かるような解説記事を書くのが大変か、 身に染みてよく理解できますねえ。 書籍執筆者には頭が下がる思いです。

本日のツッコミ(全2件) [ツッコミを入れる]
_ f# (2006-12-13 20:57)

正統な使い方なのかは知らないのですが、 __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'

_ okuji (2006-12-14 03:32)

おお、確かにそうですね。ありがとうございます。<br>本来の意図はdictオブジェクトを小さいインスタンスに必ず作るのは無駄が多いので云々とリファレンスには書いてますよね。でもやりたかったことは__slots__で十分できるわけで、それでいいような気がします。

[]

トップ «前の日記(2006-11-30) 最新 次の日記(2007-01-01)»