一个像字典一样的Python类

163

我想编写一个类,其行为与dict相似 - 因此,我正在从dict继承。

我的问题是:我需要在__init__()方法中创建一个私有的dict成员吗?我不明白这样做的意义,因为如果我简单地继承dict,我已经拥有了dict的行为。

有人可以解释一下为什么大多数继承片段看起来像下面的代码吗?

class CustomDictOne(dict):
   def __init__(self):
      self._mydict = {} 

   # other methods follow

与其使用简单的 setTimeout,更好的方式是使用 requestAnimationFrame 来制作动画。这可以帮助你避免在更新过程中出现性能问题。

class CustomDictTwo(dict):
   def __init__(self):
      # initialize my other stuff here ...

   # other methods follow

实际上,我认为我怀疑问题的答案是为了防止用户直接访问您的字典(即他们必须使用您提供的访问方法)。

然而,那么数组访问运算符[]怎么办呢?如何实现它?到目前为止,我还没有看到一个示例展示如何覆盖[]操作符。

因此,如果在自定义类中未提供[]访问函数,则继承的基本方法将操作不同的字典?

我尝试了以下代码片段来测试我的Python继承理解:

class myDict(dict):
    def __init__(self):
        self._dict = {}

    def add(self, id, val):
        self._dict[id] = val


md = myDict()
md.add('id', 123)
print md[id]

我遇到了以下错误:

KeyError: < built-in function id>

上述代码有什么问题?

如何更正myDict类,以便我可以编写如下的代码?

md = myDict()
md['id'] = 123

[编辑]

我已经编辑了上面的代码示例,摆脱了之前从桌子旁边匆忙离开时犯的愚蠢错误。这是一个打字错误(我应该从错误消息中看出来的)。

10个回答

146
class Mapping(dict):

    def __setitem__(self, key, item):
        self.__dict__[key] = item

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

    def __repr__(self):
        return repr(self.__dict__)

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

    def __delitem__(self, key):
        del self.__dict__[key]

    def clear(self):
        return self.__dict__.clear()

    def copy(self):
        return self.__dict__.copy()

    def has_key(self, k):
        return k in self.__dict__

    def update(self, *args, **kwargs):
        return self.__dict__.update(*args, **kwargs)

    def keys(self):
        return self.__dict__.keys()

    def values(self):
        return self.__dict__.values()

    def items(self):
        return self.__dict__.items()

    def pop(self, *args):
        return self.__dict__.pop(*args)

    def __cmp__(self, dict_):
        return self.__cmp__(self.__dict__, dict_)

    def __contains__(self, item):
        return item in self.__dict__

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

    def __unicode__(self):
        return unicode(repr(self.__dict__))


o = Mapping()
o.foo = "bar"
o['lumberjack'] = 'foo'
o.update({'a': 'b'}, c=44)
print 'lumberjack' in o
print o

In [187]: run mapping.py
True
{'a': 'b', 'lumberjack': 'foo', 'foo': 'bar', 'c': 44}

54
如果你要创建dict的子类,那么你应该使用对象本身(使用super),而不是简单地委托给实例的__dict__ - 这实质上意味着你为每个实例创建了两个字典。 - Russia Must Remove Putin
27
self.__dict__不等同于实际的字典内容。每个Python对象,无论其类型如何,都有一个__dict__属性,其中包含所有对象属性(方法,字段等)。除非你想编写修改自身代码的代码,否则不应该随意更改它。 - Raik

120

像这样

class CustomDictOne(dict):
   def __init__(self,*arg,**kw):
      super(CustomDictOne, self).__init__(*arg, **kw)

现在你可以使用内置函数,例如dict.get()作为self.get()

您不需要包装一个隐藏的self._dict。您的类已经是一个字典。


6
这个意思是,在继承“dict”之前,没有必要不调用它的构造函数。 - sykora
1
请注意,您继承的 dict 实际上包含两个 dict 实例:第一个是继承的容器,第二个是保存类属性的字典 - 您可以通过使用 slots 来避免这种情况。 - ankostis
3
只有在第一次访问时才会创建__dict__,所以只要用户不尝试使用它就可以了。但是有__slots__会更好。 - Russia Must Remove Putin
54
使用逗号后要加空格,你这个蛮荒之人!;-) - James Burke
@AaronHall 或 @ankostis,你们能否详细说明如何将 __slots__ 添加到上述代码中?我已经查看了其他帖子并尝试了一些操作,但目前还没有成功。 - user12446118
2
@andrewtavis 我已经发布了一个演示插槽的答案。 - Russia Must Remove Putin

49

请查看模拟容器类型的文档。在您的情况下,add的第一个参数应该是self


为了完整起见,这里是 @björn-pollex 提到的最新 Python 2.x(截至撰写本文时为 2.7.7)文档链接:模拟容器类型。(很抱歉没有使用评论功能,我在 stackoverflow 上没有权限。) - he1ix

11

这是我最好的解决方案。我用过很多次。

class DictLikeClass:
    ...
    def __getitem__(self, key):
        return getattr(self, key)

    def __setitem__(self, key, value):
        setattr(self, key, value)
    ...

你可以使用如下方式:
>>> d = DictLikeClass()
>>> d["key"] = "value"
>>> print(d["key"])

10
Python标准库中的UserDict旨在实现此目的。

使用UserDict作为基类而不是dict,可以获得一个"data"属性,以便在需要将您的类处理为字典时与其交互(例如,如果您想要对字典数据进行json编码)。 - nicolaum

9
这里提供一种备选方案:
class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__dict__ = self

a = AttrDict()
a.a = 1
a.b = 2

这很糟糕,因为你没有定义任何自定义方法,而且还有其他问题,正如其他答案所说。 - Shital Shah
这正是我所需要的。谢谢! - jakebrinkmann

9

一个类似于dict的Python类

这个有什么问题?

有没有人能指出为什么大多数继承片段看起来像下面这样?

class CustomDictOne(dict):
   def __init__(self):
      self._mydict = {} 

可以推测有充分的理由继承字典(也许你已经在使用一个字典,并且想要一种更具体的字典类型),并且您有充分的理由实例化另一个字典以委托它(因为这将实例化每个类的两个字典)。但是这听起来不正确,对吗?

我自己从未遇到过这种情况。我很喜欢使用可输入类型的字典,但在这种情况下,我更喜欢具有类型的类属性的想法,而字典的整个重点是您可以给它任何可哈希类型的键和任何类型的值。

那么为什么我们会看到像这样的代码片段呢?我个人认为,这是一个容易犯的错误,没有得到纠正,因此随着时间的推移而一直存在。

为了通过继承展示代码重用,我宁愿看到这个:

class AlternativeOne(dict):
    __slots__ = ()
    def __init__(self):
        super().__init__()
        # other init code here
    # new methods implemented here

或者,为了展示重新实现字典行为的方法,可以使用以下代码:
from collections.abc import MutableMapping 

class AlternativeTwo(MutableMapping):
    __slots__ = '_mydict'
    def __init__(self):
        self._mydict = {}
        # other init code here
    # dict methods reimplemented and new methods implemented here

应要求 - 添加插槽到字典子类。

为什么要添加插槽?内置的dict实例没有任意属性:

>>> d = dict()
>>> d.foo = 'bar'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'dict' object has no attribute 'foo'

如果我们像大多数人在这个答案中创建子类,我们会发现我们没有获得相同的行为,因为我们将拥有一个__dict__属性,这会导致我们的字典占用可能多达两倍的空间:

my_dict(dict):
    """my subclass of dict""" 

md = my_dict()
md.foo = 'bar'

自上述代码没有引发错误,所以上述类实际上不像“dict”那样运行。
我们可以通过提供空插槽(empty slots)来使其像字典一样运行:
class my_dict(dict):
    __slots__ = ()

md = my_dict()

因此,现在尝试使用任意属性将失败:

>>> md.foo = 'bar'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'my_dict' object has no attribute 'foo'

这个Python类更像一个dict

有关如何以及为什么使用slots的更多信息,请参见此问答:使用__slots__?


2
我真的找不到正确的答案。
class MyClass(dict):
    
    def __init__(self, a_property):
        self[a_property] = a_property

你需要做的就是定义自己的__init__函数,这就是它的全部。

另一个例子(稍微复杂一些):

class MyClass(dict):

    def __init__(self, planet):
        self[planet] = planet
        info = self.do_something_that_returns_a_dict()
        if info:
            for k, v in info.items():
                self[k] = v

    def do_something_that_returns_a_dict(self):
        return {"mercury": "venus", "mars": "jupiter"}

当你想要嵌入某种逻辑时,最后一个例子非常方便。

总之,简单来说,class GiveYourClassAName(dict)就足以使你的类表现得像一个字典。你在 self 上执行的任何字典操作都会像普通字典一样。


1
这段代码的问题是:
class myDict(dict):
    def __init__(self):
        self._dict = {}

    def add(id, val):
        self._dict[id] = val


md = myDict()
md.add('id', 123)

...是因为你的“add”方法(以及任何你想要成为类的成员的方法)需要在第一个参数中显式声明“self”,就像这样:

def add(self, 'id', 23):

要实现通过键访问项目的运算符重载,请查看文档中的魔术方法__getitem____setitem__

请注意,由于Python使用鸭子类型,实际上可能没有理由从语言的dict类派生自定义dict类--除非您更了解您要做什么(例如,如果您需要将此类的实例传递到某些代码中,这些代码将会中断,除非isinstance(MyDict(), dict) == True),否则您最好只实现使您的类具有足够类似字典的API并停止在那里。


0

永远不要继承Python内置的dict!例如,update方法不会使用__setitem__,它们会进行很多优化。请使用UserDict。

from collections import UserDict

class MyDict(UserDict):
    def __delitem__(self, key):
        pass
    def __setitem__(self, key, value):
        pass

4
有哪些情况下,某人永远不应该从内置字典中继承?根据文档(https://docs.python.org/3/library/collections.html#collections.UserDict):“这个类的需求部分被直接从dict子类化的能力所取代;但是,由于底层字典可以作为属性访问,因此使用这个类可能更容易。” 同样在该页面上:集合模块“自版本3.3开始已过时,将在版本3.9中删除:将集合抽象基类移动到collections.abc模块。”…其中没有UserDict。 - NumesSanguis
我怀疑这受到了启发,或者至少与 https://treyhunner.com/2019/04/why-you-shouldnt-inherit-from-list-and-dict-in-python/ 有共鸣。 - tripleee
关于第二点:废弃整个模块并将其放在一个命名空间级别更深的位置是没有什么意义的。废弃警告适用于以前位于collections中现在位于collections.abc中的抽象基类(ABC)。这适用于collections.abc中的每个类。UserDict不是ABC,Counterdefaultdict也不是。完整的模块废弃在文档中会有非常不同的说明。 - Andras Deak -- Слава Україні

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