如何使字典只读?

41

我有一个类如下:

class A:
    def __init__(self):
        self.data = {}

某个时刻,我想禁止修改self.data字段。

我在PEP-416拒绝通知中读到有很多方法可以实现。所以我想找出它们是什么。

我尝试了这个:

a = A()
a.data = types.MappingProxyType(a.data)

那应该可以使用,但首先,它需要 Python3.3 及以上版本;其次,当我多次执行这个“禁止”操作时,会出现以下情况:

>>> a.data = types.MappingProxyType(a.data)
>>> a.data = types.MappingProxyType(a.data)
>>> a.data
mappingproxy(mappingproxy({}))
虽然最好只得到mappingproxy({}),因为我将会频繁地“禁止”许多次。检查isinstance(MappingProxyType)是一个选项,但我认为可能存在其他选项。
谢谢。

1
这篇文章https://dev59.com/h17Va4cB1Zd3GeqPO_kC 可能会对你有所帮助。 - Garfield
3
一个 frozendict 的实现看起来相当简单:https://github.com/slezica/python-frozendict/blob/master/frozendict/__init__.py - georg
2
@codelover 没错,我可以用 throw NotImplemented 来重写 __setitem__。但是有没有保证在任何 Python 实现中,通过任何标准的 dict 属性都无法修改键或值呢? - sshilovsky
1
@sshilovsky 不是的,相反你可以派生一个用户字典并覆盖任何可以应用的地方。 - Garfield
2
这个回答解决了你的问题吗?“frozen dict”是什么意思?(https://dev59.com/wHE85IYBdhLWcg3wl0nF) - Seanny123
6个回答

46

使用 collections.Mapping,例如:

import collections

class DictWrapper(collections.Mapping):

    def __init__(self, data):
        self._data = data

    def __getitem__(self, key): 
        return self._data[key]

    def __len__(self):
        return len(self._data)

    def __iter__(self):
        return iter(self._data)

它是否仅创建字典本身,但并不一定使包含的字典不可变?我当前想要创建一个表示配置的嵌套字典是不可变的。 - Martin Thoma
1
我认为它已经完全按照你所描述的方式工作了。dict_wrapper[key] 返回你放置在那里的任何内容,这可能是一个原始的 dict - sshilovsky
好的回答!值得一提的是:(1)现代Python使用collections.abc.Mapping,而不是已被弃用的collections.Mapping。(2)collections.abc.Mapping根本没有定义__setitem__或类似的方法,因此尝试将数据存储到映射中将导致TypeError异常。(3)如果您想要使整个字典树变为只读,您需要使__gititem__返回一个列表和字典的只读包装器。此外,您还需要定义一个只读迭代器以返回__iter__ - Jonathan Mayer

39

这是快速(浅层)只读字典的完整实现:

def _readonly(self, *args, **kwargs):
    raise RuntimeError("Cannot modify ReadOnlyDict")

class ReadOnlyDict(dict):
    __setitem__ = _readonly
    __delitem__ = _readonly
    pop = _readonly
    popitem = _readonly
    clear = _readonly
    update = _readonly
    setdefault = _readonly

我之前(更差的)实现如下(感谢 @mtraceur 的好评!):

class ReadOnlyDict(dict):
    def __readonly__(self, *args, **kwargs):
        raise RuntimeError("Cannot modify ReadOnlyDict")
    __setitem__ = __readonly__
    __delitem__ = __readonly__
    pop = __readonly__
    popitem = __readonly__
    clear = __readonly__
    update = __readonly__
    setdefault = __readonly__
    del __readonly__

为了允许使用copy.copycopy.deepcopy进行复制,您可能需要添加以下两行代码:__copy__ = dict.copy__deepcopy__ = copy._deepcopy_dispatch.get(dict)。尽管没有经过太多测试。 - blueyed
没错,但是如何初始化它或者如何让现有的字典使用它呢? - mari.mts
4
ro_dict = ReadOnlyDict(rw_dict) - Marcin Raczyński
1
执行 del __readonly__ 会影响方法的 pickling,包括未绑定和已绑定的方法,如果有的话。此外,所有形式为 __*__ 的名称都由 Python 保留,并且随时可能被更改。因此,我会将其称为 _readonly,在类外定义它,并将其留在定义它的模块中,而不是删除它。(Bound 函数仍然无法 unpickle,但这是由于 Python 对绑定方法的 pickling 方法存在问题 - 对这些方法的未绑定引用将是可 pickle 的。) - mtraceur
好的,刚刚注意到了编辑,很高兴能帮忙! - mtraceur
这个不反映最新的更新,请参考-https://gist.github.com/nduhamel/748954 - Dave Ankin

10

非常容易,您只需要覆盖默认字典的方法!
这是一个示例:


class ReadOnlyDict(dict):

    __readonly = False

    def readonly(self, allow=1):
        """Allow or deny modifying dictionary"""
        self.__readonly = bool(allow)

    def __setitem__(self, key, value):

        if self.__readonly:
            raise TypeError, "__setitem__ is not supported"
        return dict.__setitem__(self, key, value)

    def __delitem__(self, key):

        if self.__readonly:
            raise TypeError, "__delitem__ is not supported"
        return dict.__delitem__(self, key)

顺便说一句,你也可以移除 .pop, .update 以及其他需要的方法。尽情摆弄它。


你如何应用“在某个时刻”?我猜 OP 需要将 dict 设为可读写,然后在一段时间后将其冻结。 - justhalf
如果他需要的话,他也可以对它们进行修改。 - JadedTuna
4
这种方法很明显,但并不简单,因为我需要覆盖几乎每个dict方法,甚至要重新实现dict的视图方法,如.keys().items()等等。而且如果Guido在未来的Python版本中添加了更多内容怎么办?无论如何,感谢这个观点,但是我希望有更多的答案。 - sshilovsky

3
最好的方法是像这样从 UserDict 派生出来:
from collections import UserDict

class MyReadOnlyDict(UserDict):
   def my_set(self, key, val, more_params):
       # do something special
       # custom logic etc
       self.data[key] = val

   def __setitem__(self, key, val):
       raise NotImplementedError('This dictionary cannot be updated')

   def __delitem__(self, key):
       raise NotImplementedError('This dictionary does not allow delete')

这种方法的优势在于您仍然可以在类内部使用方法来访问self.data并更新字典。

instance = MyReadOnlyDict(); instance.data['a'] = 'b' 这样会怎么样? - Vladimir Prudnikov

1

怎么样?这是 @mouad 的回答的更新。

import json
from collections import OrderedDict
from collections.abc import Mapping


class ReadOnlyJsonObject(Mapping):
    def __init__(self, data, dumps_kw: dict=None, loads_kw: dict=None):
        if dumps_kw is None:
            dumps_kw = dict()
        if loads_kw is None:
            self._loads_kw = dict(object_pairs_hook=OrderedDict)
        else:
            self._loads_kw = loads_kw

        if isinstance(data, str):
            self._json_string = data
        else:
            self._json_string = json.dumps(data, **dumps_kw)

    @property
    def _data(self):
        return json.loads(self._json_string, **self._loads_kw)

    def __getitem__(self, key):
        return self._data[key]

    def __len__(self):
        return len(self._data)

    def __iter__(self):
        return iter(self._data)

    def __str__(self):
        return self._json_string

虽然不确定性能如何,但我在一个真实项目中使用了这个https://github.com/patarapolw/AnkiTools/blob/master/AnkiTools/tools/defaults.py


0

迟来的回答。您可以使用属性和MappingProxyType的组合来保护.data字段免受修改。

from types import MappingProxyType

class A:
    def __init__(self):
        self._data = {}

    @property
    def data(self):
        return MappingProxyType(self._data)

    def update(self, key, value):
        self._data[key] = value

a = A()
a.update("foo", 1)
a.update("bar", 2)
print(f"a.data={a.data}")

# Attempt to modify a.data directly will fail
try:
    a.data["bar"] = 20
except TypeError as error:
    print(f"Error: {error}")

# Attempt to assign something else to a.data, will also fail
try:
    a.data = {}
except AttributeError as error:
    print(f"Error: {error}")

输出:

a.data={'foo': 1, 'bar': 2}
Error: 'mappingproxy' object does not support item assignment
Error: property 'data' of 'A' object has no setter

注意事项

  • A.data不是一个普通的属性,而是一个属性。由于没有setter,它也是一个只读属性。
  • A.data返回的是一个MappingProxy而不是字典本身。这意味着类外的代码将无法修改字典的内容。
  • 为了修改字典,我添加了update()方法作为示例。
  • 如果人们想要的话,他们可以直接修改A._data,我们无法阻止这样做。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接