You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 17:42:47