如何"完美"覆盖一个字典?

Pau*_*gar 204 python inheritance dictionary get set

我怎样才能使"完美"成为dict的子类?最终目标是有一个简单的字典,其中键是小写的.

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

这是我的第一次尝试,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()
Run Code Online (Sandbox Code Playgroud)

Joc*_*zel 215

您可以使用collections模块中的ABC(抽象基类)轻松编写一个行为类似于dict的对象.它甚至会告诉你是否错过了一个方法,所以下面是关闭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
Run Code Online (Sandbox Code Playgroud)

你从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
Run Code Online (Sandbox Code Playgroud)

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

  • 我建议重命名`__keytransform __()`,因为它违反了PEP 8样式指南,该指南建议"永远不要发明这些名称;只在[描述:命名样式]的末尾使用它们"(http://www.python) .org/dev/peps/pep-0008/#descriptive-naming-styles)部分. (39认同)
  • @AndyHayden:你应该写`if isinstance(t,collections.MutableMapping):print t,"可以像dict一样使用"`.不检查对象的类型,检查界面. (5认同)
  • 有没有一种方法可以做到isinstance(_,dict)== True?还是只使用可变映射来构造子类? (2认同)
  • @NeilG不幸的是,它在python标准库中包含JSONEncoder-https://github.com/python-git/python/blob/715a6e5035bb21ac49382772076ec4c630d6e960/Lib/json/encoder.py#L308 (2认同)

Aar*_*all 89

我怎样才能使"完美"成为dict的子类?

最终目标是有一个简单的字典,其中键是小写的.

  • 如果我覆盖__getitem__/ __setitem__,则get/set不起作用.我如何让它们工作?当然我不需要单独实施它们?

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

  • 我需要repr,更新和__init__

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

接受的答案将是我的第一个方法,但由于它有一些问题,并且因为没有人解决了替代方案,实际上是继承a dict,我将在这里做.

接受的答案有什么问题?

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

我怎样才能使"完美"成为dict的子类?最终目标是有一个简单的字典,其中键是小写的.

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

>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
Run Code Online (Sandbox Code Playgroud)

理想情况下,任何类型检查代码都将测试我们期望的接口或抽象基类,但是如果我们的数据对象被传递到正在测试的函数中dict- 并且我们无法"修复"这些函数,则此代码将失败.

人们可能做出的其他狡辩:

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

    >>> s.foo = 'bar'
    >>> s.__dict__
    {'foo': 'bar', 'store': {'test': 'test'}}
    
    Run Code Online (Sandbox Code Playgroud)

实际上是子类化 dict

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

如果我覆盖__getitem__/ __setitem__,则get/set不起作用.我如何让它们工作?当然我不需要单独实施它们?

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

首先,让我们分解Python 2和3之间的区别,创建一个singleton(_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
Run Code Online (Sandbox Code Playgroud)

现在我们实现 - 我正在使用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__())
Run Code Online (Sandbox Code Playgroud)

我们使用的样板化的方式为引用的一个关键任何方法或特殊的方法,但在其他方面,通过继承,我们获得方法:len,clear,items,keys,popitem,和values是免费的.虽然这需要一些仔细考虑才能做到正确,但要看到这一点很有用,这是微不足道的.

(注意,haskeyPython 2 中已弃用,在Python 3中已删除.)

这是一些用法:

>>> 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)
Run Code Online (Sandbox Code Playgroud)

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

酸洗

而dict子类泡菜就好了:

>>> 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'>
Run Code Online (Sandbox Code Playgroud)

__repr__

我需要repr,更新和__init__

我们定义update__init__,但你有一个美丽的__repr__默认:

>>> ld # without __repr__ defined for the class, we get this
{'foo': None}
Run Code Online (Sandbox Code Playgroud)

但是,编写一个__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
Run Code Online (Sandbox Code Playgroud)

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

>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
Run Code Online (Sandbox Code Playgroud)

结论

我应该使用mutablemapping(似乎不应该使用UserDictDictMixin)?如果是这样,怎么样?文档并不完全具有启发性.

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

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

我应该补充说,有一个推动将类似的字典放入collections模块,但它被拒绝了.你可能应该这样做:

my_dict[transform(key)]
Run Code Online (Sandbox Code Playgroud)

它应该更容易调试.

比较和对比

有6个接口函数MutableMapping(缺少fromkeys)和11 dict个子类实现.我并不需要实现__iter__或者__len__,而是我要实现get,setdefault,pop,update,copy,__contains__,和fromkeys-但这些都是相当琐碎,因为我可以使用继承大多数这些实现的.

MutableMapping工具在Python一些事情,dict用C实现了-所以我希望一个dict子类在某些情况下更好的性能.

我们__eq__在两种方法中都获得了自由- 只有当另一个dict全部小写时才假设相等 - 但是我认为dict子类将更快地进行比较.

摘要:

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

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


Mr_*_*s_D 6

我的要求有点严格:

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

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

  • 事实证明很难做到这一点 - 请参阅:python 中不区分大小写的字符串类
  • 事实证明,显式的 dict 键处理使代码变得冗长和凌乱 - 并且容易出错(结构被到处传递,并且不清楚它们是否将 CIStr 实例作为键/元素,容易忘记加上some_dict[CIstr(path)]是丑陋的)

所以我最终不得不写下那个不区分大小写的字典。多亏了@AaronHall 的代码,这让代码变得简单了 10 倍。

class CIstr(unicode):
    """See /sf/answers/3018561381/, especially for inlines"""
    __slots__ = () # does make a difference in memory performance

    #--Hash/Compare
    def __hash__(self):
        return hash(self.lower())
    def __eq__(self, other):
        if isinstance(other, CIstr):
            return self.lower() == other.lower()
        return NotImplemented
    def __ne__(self, other):
        if isinstance(other, CIstr):
            return self.lower() != other.lower()
        return NotImplemented
    def __lt__(self, other):
        if isinstance(other, CIstr):
            return self.lower() < other.lower()
        return NotImplemented
    def __ge__(self, other):
        if isinstance(other, CIstr):
            return self.lower() >= other.lower()
        return NotImplemented
    def __gt__(self, other):
        if isinstance(other, CIstr):
            return self.lower() > other.lower()
        return NotImplemented
    def __le__(self, other):
        if isinstance(other, CIstr):
            return self.lower() <= other.lower()
        return NotImplemented
    #--repr
    def __repr__(self):
        return '{0}({1})'.format(type(self).__name__,
                                 super(CIstr, self).__repr__())

def _ci_str(maybe_str):
    """dict keys can be any hashable object - only call CIstr if str"""
    return CIstr(maybe_str) if isinstance(maybe_str, basestring) else maybe_str

class LowerDict(dict):
    """Dictionary that transforms its keys to CIstr instances.
    Adapted from: /sf/answers/2756301201/
    """
    __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, 'iteritems'):
            mapping = getattr(mapping, 'iteritems')()
        return ((_ci_str(k), v) for k, v in
                chain(mapping, getattr(kwargs, 'iteritems')()))
    def __init__(self, mapping=(), **kwargs):
        # dicts take a mapping or iterable as their optional first argument
        super(LowerDict, self).__init__(self._process_args(mapping, **kwargs))
    def __getitem__(self, k):
        return super(LowerDict, self).__getitem__(_ci_str(k))
    def __setitem__(self, k, v):
        return super(LowerDict, self).__setitem__(_ci_str(k), v)
    def __delitem__(self, k):
        return super(LowerDict, self).__delitem__(_ci_str(k))
    def copy(self): # don't delegate w/ super - dict.copy() -> dict :(
        return type(self)(self)
    def get(self, k, default=None):
        return super(LowerDict, self).get(_ci_str(k), default)
    def setdefault(self, k, default=None):
        return super(LowerDict, self).setdefault(_ci_str(k), default)
    __no_default = object()
    def pop(self, k, v=__no_default):
        if v is LowerDict.__no_default:
            # super will raise KeyError if no default and key does not exist
            return super(LowerDict, self).pop(_ci_str(k))
        return super(LowerDict, self).pop(_ci_str(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__(_ci_str(k))
    @classmethod
    def fromkeys(cls, keys, v=None):
        return super(LowerDict, cls).fromkeys((_ci_str(k) for k in keys), v)
    def __repr__(self):
        return '{0}({1})'.format(type(self).__name__,
                                 super(LowerDict, self).__repr__())
Run Code Online (Sandbox Code Playgroud)

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

欢迎评论/更正:)


rav*_*404 6

你所要做的就是

class BatchCollection(dict):
    def __init__(self, *args, **kwargs):
        dict.__init__(*args, **kwargs)
Run Code Online (Sandbox Code Playgroud)

或者

class BatchCollection(dict):
    def __init__(self, inpt={}):
        super(BatchCollection, self).__init__(inpt)
Run Code Online (Sandbox Code Playgroud)

我个人使用的示例用法

### 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")
Run Code Online (Sandbox Code Playgroud)

注意:仅在 python3 中测试


Gro*_*oxx 5

在尝试了最重要的 两个建议之后,我已经为 Python 2.7 确定了一条阴暗的中间路线。也许 3 更理智,但对我来说:

class MyDict(MutableMapping):
   # ... the few __methods__ that mutablemapping requires
   # and then this monstrosity
   @property
   def __class__(self):
       return dict
Run Code Online (Sandbox Code Playgroud)

我真的很讨厌,但似乎符合我的需求,它们是:

  • 可以覆盖 **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
Run Code Online (Sandbox Code Playgroud)

只要你只需要在内部识别你自己,这样就更难__am_i_me由于 python 的名称调整而意外调用(这被重命名为_MyDict__am_i_me来自该类之外的任何调用)。_method在实践和文化上都比s更私密。

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


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

基本上:复制当前的 #2 optionprint 'method_name'向每个方法添加行,然后尝试此操作并观察输出:

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

对于其他场景,您会看到类似的行为。假设您的 fake-dict是一些其他数据类型的包装器,因此没有合理的方法将数据存储在 backing-dict 中;**your_dict将是空的,不管其他方法做什么。

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


编辑:作为更新,这已经运行了将近两年没有任何问题,在几十万(呃,可能是几百万)行复杂的、遗留问题的 python 上。所以我很满意:)

编辑 2:显然我很久以前就错误地复制了这个或其他东西。 @classmethod __class__不适用于isinstance检查 -@property __class__是否:https : //repl.it/repls/UnitedScientificSequence