読者です 読者をやめる 読者になる 読者になる

OrderedDictのバグ?

OrderdDictで遊んでたら、下のような現象に遭遇しました。

>>> from collections import OrderedDict
>>> d = OrderedDict(one=1, two=2, three=3)
>>> d
OrderedDict([('one', 1), ('three', 3), ('two', 2)])
>>> d['one']
1
>>> d.one=100
>>> d
OrderedDict([('one', 1), ('three', 3), ('two', 2)])
>>> d.one
100

え、ナニコレ…

obj[name] と obj.name

OrderedDictには「キー:値」以外にも「属性=値」で値を保持できてしまうんですね。ただ、これはOrderedDictに限ったことではなくて、dictを継承したクラスなんかを自作した場合も同じでした。

他の言語、特にLuaみたいなobj[name]とobj.nameのどちらの方法でも値を取り出せる言語からPythonに戻ってくると、つい obj.name = value とやってしまいがちなので気を付けないといけないですね。Luaはどちらの方法でも同じ値を参照してるけど、Pythonの場合obj[name]が持つ値とobj.nameが持つ値は全く別物なので。

ちなみにdefaultdictや普通の辞書は obj.name = value なんてやろうとするとAttributeErrorになります。

>>> d = dict(one=1, two=2)
>>> d.one = 100

Traceback (most recent call last):
  File "<pyshell#9>", line 1, in <module>
    d.one = 100
AttributeError: 'dict' object has no attribute 'one'

>>> from collections import defaultdict
>>> d = defaultdict(one=1, two=2)
>>> d.one = 100

Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    d.one = 100
AttributeError: 'collections.defaultdict' object has no attribute 'one'

dictを継承した自作クラスに新たな属性を禁止する

とりあえず、辞書を継承してるんだから値を取り出すときはキーだけでいいだろうということで、新たな属性の追加を禁止しておくといいのかな、と。

class MyDict(dict):

    def __setattr__(self, name, value):
        if not hasattr(self, name):
            raise(AttributeError(
                "'{0}' object has no attribute '{1}'".format(
                    self.__class__.__name__, name)))


if __name__ == '__main__':
    md = MyDict(one=1, two=2, three=3)
    print(md)
    md.four=4 #AttributeError: 'MyDict' object has no attribute 'four'

とりあえずこんな感じで新たな属性の追加を禁止することができました。OrderedDictもこんな感じで新たな属性の追加を禁止したほうがいいんじゃないかと思うんですが、どうなんでしょう。

※追記

__slots__ を使う方法もありますよ、とアドバイスをいただきました。
データモデル — Python 2.7ja1 documentation

__slots__ に渡したシーケンスの要素名のみがインスタンスの属性として存在できるようなので、空のシーケンスを与えてやれば一切の新属性を追加禁止にできるようです。ただし、__slots__を持たないクラスを継承している場合は__dict__は常にアクセス可能なので意味が無いとか。

class MyDict(dict):
    __slots__ = ()
    ...

通常の辞書にちょっとしたメソッドを追加したクラスを定義するような場合であれば問題なく使えそうですね。