如何“完美”覆盖一个字典?

时间:2010-08-02 12:23:28

标签: python inheritance dictionary get set

如何尽可能“完美”成为 dict 的子类?最终目标是拥有一个简单的 dict ,其中键是小写的。

似乎应该有一些我可以覆盖的微小原语来使这项工作,但根据我的所有研究和尝试,似乎情况并非如此:

  • 如果我override __getitem__/__setitem__,则get / set无效。我怎样才能让他们工作?当然我不需要单独实施它们吗?

  • 我是否阻止酸洗工作,我是否需要实施__setstate__等?

  • need repr, update and __init__吗?

  • 我应该use mutablemapping(似乎不应该使用UserDict 还是DictMixin)?如果是这样,怎么样?文档并不完全具有启发性。

以下是我的第一篇文章,get()不起作用,毫无疑问还有许多其他小问题:

class arbitrary_dict(dict):
    """A dictionary that applies an arbitrary key-altering function
       before accessing the keys."""

    def __keytransform__(self, key):
        return key

    # Overridden methods. List from 
    # https://stackoverflow.com/questions/2390827/how-to-properly-subclass-dict

    def __init__(self, *args, **kwargs):
        self.update(*args, **kwargs)

    # Note: I'm using dict directly, since super(dict, self) doesn't work.
    # I'm not sure why, perhaps dict is not a new-style class.

    def __getitem__(self, key):
        return dict.__getitem__(self, self.__keytransform__(key))

    def __setitem__(self, key, value):
        return dict.__setitem__(self, self.__keytransform__(key), value)

    def __delitem__(self, key):
        return dict.__delitem__(self, self.__keytransform__(key))

    def __contains__(self, key):
        return dict.__contains__(self, self.__keytransform__(key))


class lcdict(arbitrary_dict):
    def __keytransform__(self, key):
        return str(key).lower()

5 个答案:

答案 0 :(得分:193)

使用ABC s可以很容易地编写一个行为类似于dict的对象 来自collections模块的(抽象基类)。它甚至告诉你 如果你错过了一个方法,那么下面是关闭ABC的最小版本。

import collections


class TransformedDict(collections.MutableMapping):
    """A dictionary that applies an arbitrary key-altering
       function before accessing the keys"""

    def __init__(self, *args, **kwargs):
        self.store = dict()
        self.update(dict(*args, **kwargs))  # use the free update to set keys

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

    def __setitem__(self, key, value):
        self.store[self.__keytransform__(key)] = value

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

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

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

    def __keytransform__(self, key):
        return key

您可以从ABC获得一些免费方法:

class MyTransformedDict(TransformedDict):

    def __keytransform__(self, key):
        return key.lower()


s = MyTransformedDict([('Test', 'test')])

assert s.get('TEST') is s['test']   # free get
assert 'TeSt' in s                  # free __contains__
                                    # free setdefault, __eq__, and so on

import pickle
assert pickle.loads(pickle.dumps(s)) == s
                                    # works too since we just use a normal dict

我不会直接将dict(或其他内置函数)子类化。它通常没有意义,因为你真正想做的是实现dict的接口。而这正是ABCs的用途。

答案 1 :(得分:76)

  

我怎样才能做到完美"尽可能使用dict的子类?

     

最终目标是拥有一个简单的dict,其中键是小写的。

     
      
  • 如果我覆盖__getitem__ / __setitem__,则获取/设置不起作用。怎么样   我让他们工作?当然,我不需要实施它们   单独

  •   
  • 我是否阻止酸洗工作,我是否需要实施   __setstate__等?

  •   
  • 我是否需要重播,更新和__init__

  •   
  • 我应该使用mutablemapping(似乎不应该使用UserDict   还是DictMixin)?如果是这样,怎么样?文档并不具有启发性。

  •   

接受的答案将是我的第一个方法,但由于它有一些问题, 并且由于没有人提到替代方案,实际上是对dict进行了子类化,我将在这里做。

接受的答案有什么问题?

这对我来说似乎是一个相当简单的要求:

  

我怎样才能做到完美"尽可能使用dict的子类?   最终目标是有一个简单的字典,其中键是小写的。

接受的答案实际上并不是dict的子类,对此的测试失败了:

>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False

理想情况下,任何类型检查代码都将测试我们期望的接口或抽象基类,但是如果我们的数据对象被传递到正在测试dict的函数 - 我们可以&#39 ; t"修复"那些功能,这段代码就会失败。

可能会有其他狡辩:

  • 接受的答案也缺少类方法:fromkeys
  • 接受的答案也有冗余__dict__ - 因此在内存中占用更多空间:

    >>> s.foo = 'bar'
    >>> s.__dict__
    {'foo': 'bar', 'store': {'test': 'test'}}
    

实际上是对dict

进行子类化

我们可以通过继承重用dict方法。我们需要做的就是创建一个接口层,确保键以小写形式传递到dict中(如果它们是字符串)。

  

如果我覆盖__getitem__ / __setitem__,则获取/设置不起作用。我如何让它们工作?当然,我不需要单独实施它们吗?

好吧,单独实施这些方法是这种方法的缺点,也是使用MutableMapping的好处(参见接受的答案),但实际上并没有那么多工作。

首先,让我们分解Python 2和3之间的区别,创建一个单例(_RaiseKeyError)以确保我们知道我们是否真正得到dict.pop的参数,并创建确保我们的字符串键是小写的函数:

from itertools import chain
try:              # Python 2
    str_base = basestring
    items = 'iteritems'
except NameError: # Python 3
    str_base = str, bytes, bytearray
    items = 'items'

_RaiseKeyError = object() # singleton for no-default behavior

def ensure_lower(maybe_str):
    """dict keys can be any hashable object - only call lower if str"""
    return maybe_str.lower() if isinstance(maybe_str, str_base) else maybe_str

现在我们实现 - 我使用super使用完整参数,以便此代码适用于Python 2和3:

class LowerDict(dict):  # dicts take a mapping or iterable as their optional first argument
    __slots__ = () # no __dict__ - that would be redundant
    @staticmethod # because this doesn't make sense as a global function.
    def _process_args(mapping=(), **kwargs):
        if hasattr(mapping, items):
            mapping = getattr(mapping, items)()
        return ((ensure_lower(k), v) for k, v in chain(mapping, getattr(kwargs, items)()))
    def __init__(self, mapping=(), **kwargs):
        super(LowerDict, self).__init__(self._process_args(mapping, **kwargs))
    def __getitem__(self, k):
        return super(LowerDict, self).__getitem__(ensure_lower(k))
    def __setitem__(self, k, v):
        return super(LowerDict, self).__setitem__(ensure_lower(k), v)
    def __delitem__(self, k):
        return super(LowerDict, self).__delitem__(ensure_lower(k))
    def get(self, k, default=None):
        return super(LowerDict, self).get(ensure_lower(k), default)
    def setdefault(self, k, default=None):
        return super(LowerDict, self).setdefault(ensure_lower(k), default)
    def pop(self, k, v=_RaiseKeyError):
        if v is _RaiseKeyError:
            return super(LowerDict, self).pop(ensure_lower(k))
        return super(LowerDict, self).pop(ensure_lower(k), v)
    def update(self, mapping=(), **kwargs):
        super(LowerDict, self).update(self._process_args(mapping, **kwargs))
    def __contains__(self, k):
        return super(LowerDict, self).__contains__(ensure_lower(k))
    def copy(self): # don't delegate w/ super - dict.copy() -> dict :(
        return type(self)(self)
    @classmethod
    def fromkeys(cls, keys, v=None):
        return super(LowerDict, cls).fromkeys((ensure_lower(k) for k in keys), v)
    def __repr__(self):
        return '{0}({1})'.format(type(self).__name__, super(LowerDict, self).__repr__())

对于引用键的任何方法或特殊方法,我们使用几乎样的锅炉方法,但是,通过继承,我们得到方法:lenclearitemskeyspopitemvalues免费提供。虽然这需要一些仔细考虑才能做到正确,但要看到这一点很有用,这是微不足道的。

(请注意,{2}在Python 2中已弃用,在Python 3中已删除。)

以下是一些用法:

haskey
  

我是否阻止酸洗工作,我是否需要实施   >>> ld = LowerDict(dict(foo='bar')) >>> ld['FOO'] 'bar' >>> ld['foo'] 'bar' >>> ld.pop('FoO') 'bar' >>> ld.setdefault('Foo') >>> ld {'foo': None} >>> ld.get('Bar') >>> ld.setdefault('Bar') >>> ld {'bar': None, 'foo': None} >>> ld.popitem() ('bar', None) 等?

酸洗

dict子类泡菜很好:

__setstate__

>>> import pickle >>> pickle.dumps(ld) b'\x80\x03c__main__\nLowerDict\nq\x00)\x81q\x01X\x03\x00\x00\x00fooq\x02Ns.' >>> pickle.loads(pickle.dumps(ld)) {'foo': None} >>> type(pickle.loads(pickle.dumps(ld))) <class '__main__.LowerDict'>

  

我需要重播,更新和__repr__吗?

我们定义了__init__update,但默认情况下您有一个漂亮的__init__

__repr__

但是,编写>>> ld # without __repr__ defined for the class, we get this {'foo': None} 以提高代码的可调试性是一件好事。理想的测试是__repr__。如果您的代码很容易,我强烈推荐它:

eval(repr(obj)) == obj

你知道,它正是我们重新创建一个等效对象所需要的 - 这可能会出现在我们的日志或回溯中:

>>> ld = LowerDict({})
>>> eval(repr(ld)) == ld
True
>>> ld = LowerDict(dict(a=1, b=2, c=3))
>>> eval(repr(ld)) == ld
True

结论

  

我应该使用>>> ld LowerDict({'a': 1, 'c': 3, 'b': 2}) (似乎不应该使用mutablemapping   还是UserDict)?如果是这样,怎么样?这些文档并不具有启发性。

是的,这些是一些代码行,但它们的目的是全面的。我的第一个倾向是使用接受的答案, 如果有问题,我会看看我的答案 - 因为它有点复杂,并且没有ABC可以帮助我正确接口。

过早优化会增加搜索性能的复杂性。 DictMixin更简单 - 所以它立即获得优势,其他条件相同。然而,为了列出所有的差异,让我们进行比较和对比。

我应该补充说,推动将类似字典放入MutableMapping模块,但it was rejected。你可能应该这样做:

collections

它应该更容易调试。

比较和对比

使用my_dict[transform(key)] (缺少MutableMapping)和使用fromkeys子类的11实现了6个接口函数。我不需要实施dict__iter__,而是我必须实施__len__getsetdefaultpopupdatecopy__contains__ - 但这些都相当简单,因为我可以在大多数实现中使用继承。

fromkeys在Python中实现了MutableMapping在C中实现的一些东西 - 所以我希望dict子类在某些情况下更有效。

我们在两种方法中都得到一个免费的dict - 只有在另一个dict全部小写的情况下才假设相等 - 但同样,我认为__eq__子类将更快地进行比较。

要点:

  • 子类化dict更简单,错误的机会更少,但速度更慢,需要更多内存(请参阅冗余dict),并且失败MutableMapping
  • 子类化isinstance(x, dict)更快,使用更少的内存,并传递dict,但实现起来更复杂。

哪个更完美?这取决于你对完美的定义。

答案 2 :(得分:3)

我的要求有点严格:

  • 我必须保留案例信息(字符串是显示给用户的文件的路径,但它是一个Windows应用程序,所以内部所有操作必须不区分大小写)
  • 我需要键尽可能小( 在内存性能上有所不同,从370中断掉110 mb)。这意味着不能选择缓存小写版本的密钥。
  • 我需要尽可能快地创建数据结构(这次又改变了性能,速度)。我不得不选择内置的

我最初的想法是将我们笨重的Path类替换为不区分大小写的unicode子类 - 但是:

  • 证明很难做到这一点 - 见:A case insensitive string class in python
  • 结果表明显式的dict键处理会使代码变得冗长和容易出错 - 并且容易出错(结构会被传递到其中,并且不清楚它们是否将CIStr实例作为键/元素,很容易忘记加{{1}是丑陋的)

所以我终于写下了不区分大小写的字典。感谢@AaronHall的code,使其变得容易了10倍。

some_dict[CIstr(path)]

隐式vs显式仍然是一个问题,但一旦尘埃落定,重命名属性/变量以ci开头(和一个大胖文档评论解释ci代表不区分大小写)我认为是一个完美的解决方案 - 作为读者的代码必须充分意识到我们正在处理不区分大小写的底层数据结构。 这将有望修复一些难以重现的错误,我怀疑归结为区分大小写。

评论/更正欢迎:)

答案 3 :(得分:2)

你所要做的只是

class BatchCollection(dict):
    def __init__(self, *args, **kwargs):
        dict.__init__(*args, **kwargs)

OR

class BatchCollection(dict):
    def __init__(self, inpt={}):
        super(BatchCollection, self).__init__(inpt)

供个人使用的样本用法

### EXAMPLE
class BatchCollection(dict):
    def __init__(self, inpt={}):
        dict.__init__(*args, **kwargs)

    def __setitem__(self, key, item):
        if (isinstance(key, tuple) and len(key) == 2
                and isinstance(item, collections.Iterable)):
            # self.__dict__[key] = item
            super(BatchCollection, self).__setitem__(key, item)
        else:
            raise Exception(
                "Valid key should be a tuple (database_name, table_name) "
                "and value should be iterable")

注意:仅在python3中测试

答案 4 :(得分:1)

在尝试了top two这两个建议之后,我已经确定了Python 2.7的阴暗中间路线。也许3更健全,但对我来说:

class MyDict(MutableMapping):
   # ... the few __methods__ that mutablemapping requires
   # and then this monstrosity
   @classmethod
   def __class__(cls):
       return dict

我真的很讨厌,但似乎符合我的需要,这是:

  • 可以覆盖**my_dict
    • 如果您从dict继承,会绕过您的代码。尝试一下。
    • 这使#2对我始终不可接受,因为这在python代码中非常常见
  • 伪装成isinstance(my_dict, dict)
    • 单独排除MutableMapping,因此#1是不够的
    • 如果你不需要,我衷心推荐#1,这很简单,可预测
  • 完全可控的行为
    • 所以我无法继承dict

如果你需要将自己与别人分开,我个人会使用这样的东西(虽然我建议更好的名字):

def __am_i_me(self):
  return True

@classmethod
def __is_it_me(cls, other):
  try:
    return other.__am_i_me()
  except Exception:
    return False

只要您只需要在内部识别自己,这样就会因为python的名称调整而意外调用__am_i_me(这会重命名为_MyDict__am_i_me来自本课外的任何事情)。在实践和文化方面都比_method略微私密。

到目前为止,我没有任何抱怨,除了严重阴暗的__class__覆盖。我很高兴听到其他人遇到的任何问题,但我并不完全理解其后果。但到目前为止,我没有任何问题,这使我能够在很多位置迁移大量中等质量的代码而无需任何更改。

作为证据:https://repl.it/repls/TraumaticToughCockatoo

基本上:复制the current #2 option,为每个方法添加print 'method_name'行,然后尝试并观察输出:

d = LowerDict()  # prints "init", or whatever your print statement said
print '------'
splatted = dict(**d)  # note that there are no prints here

您会看到其他方案的类似行为。假设您的假 - dict是其他数据类型的包装器,因此没有合理的方法将数据存储在backing-dict中;无论其他方法做什么,**your_dict都将为空。

这适用于MutableMapping,但是一旦从dict继承,它就会变得无法控制。