为何ImmutableDict的__setitem__未触发预期的不可变异常?
为什么ImmutableDict实例下标赋值能成功修改,直接调用__setitem__却抛出异常?
问题重现
以下代码尝试实现一个不可变字典ImmutableDict,通过在实例初始化时将__setitem__等方法绑定到raise_immutable来限制修改操作。但出现了矛盾现象:直接调用i.__setitem__('b',2)会抛出预期的不可变异常,然而使用下标赋值i['b']=2却能成功修改字典内容。
from typing import Mapping, TypeVar KT = TypeVar('KT') VT_co = TypeVar('VT_co', covariant=True) class Immutable: def __init__(self, *args, **kwargs): super(Immutable, self).__init__(*args, **kwargs) # set them all to self.raise_immutable self.__setitem__ = self.__delitem__ = self.setdefault = self.update = self.pop = self.popitem = self.clear = self.raise_immutable def raise_immutable(self, *args, **kwargs): clsname = self.__class__.__name__ raise TypeError(f"{clsname} objects are immutable") def __repr__(self): clsname = self.__class__.__name__ content = super(Immutable, self).__repr__() return f"{clsname}({content}" class ImmutableDict(Immutable, dict, Mapping[KT, VT_co]): def __hash__(self): return hash(tuple(self.items())) i = ImmutableDict({'a':1}) print(i) # ImmutableDict({'a': 1}) print(i.__setitem__) # <bound method Immutable.raise_immutable of ImmutableDict({'a': 1})> # i.__setitem__('b', 2) # this throws an exception as desired i['b'] = 2 # but yet this works? print(i) # ImmutableDict({'a': 1, 'b': 2})
问题原因
Python 对特殊方法(双下划线包裹的方法,如__setitem__)的查找逻辑是优先在类层面检索,而非从实例的属性中查找:
- 当使用
i['b'] = 2下标赋值语法时,Python 会直接去ImmutableDict类(或其继承链的类)中查找__setitem__方法,此时找到的是继承自dict类的原生实现,所以会正常执行修改操作。 - 而直接调用
i.__setitem__('b', 2)时,是直接访问实例对象上的__setitem__属性,这时候会调用你在初始化时绑定的raise_immutable方法,因此抛出异常。
简言之:实例上绑定的特殊方法不会被Python的语法糖(如下标赋值)触发,语法糖只会调用类层面定义的特殊方法。
修复方案
要实现真正的不可变字典,需要在类层面重写这些特殊方法,而非仅在实例上覆盖属性。修改后的代码如下:
from typing import Mapping, TypeVar KT = TypeVar('KT') VT_co = TypeVar('VT_co', covariant=True) class Immutable: # 在类层面重写所有修改类方法 def __setitem__(self, key, value): self.raise_immutable() def __delitem__(self, key): self.raise_immutable() def setdefault(self, *args, **kwargs): self.raise_immutable() def update(self, *args, **kwargs): self.raise_immutable() def pop(self, *args, **kwargs): self.raise_immutable() def popitem(self): self.raise_immutable() def clear(self): self.raise_immutable() def raise_immutable(self, *args, **kwargs): clsname = self.__class__.__name__ raise TypeError(f"{clsname} objects are immutable") def __repr__(self): clsname = self.__class__.__name__ content = super(Immutable, self).__repr__() return f"{clsname}({content})" # 修复原代码中缺失的右括号 class ImmutableDict(Immutable, dict, Mapping[KT, VT_co]): def __hash__(self): return hash(tuple(self.items())) i = ImmutableDict({'a':1}) print(i) # ImmutableDict({'a': 1}) # i['b'] = 2 # 现在会抛出 TypeError: ImmutableDict objects are immutable # i.__setitem__('b', 2) # 同样抛出异常
这样无论使用下标赋值还是直接调用__setitem__,都会触发自定义的不可变校验逻辑,真正实现字典的不可变性。
内容的提问来源于stack exchange,提问作者Captain_Obvious
相关产品推荐
相关产品推荐

