如何“完美地”覆盖字典?

2024-12-20 08:37:00
admin
原创
69
摘要:问题描述:我怎样才能使字典的子类尽可能“完美”?最终目标是拥有一个简单的字典,其中的键都是小写的。似乎应该有一些我可以覆盖的很小的原语集来使其工作,但根据我的所有研究和尝试,似乎事实并非如此:如果我覆盖__getitem__/__setitem__,则get/set不起作用。我该如何让它们工作?我肯定不需要单...

问题描述:

我怎样才能使字典的子类尽可能“完美”?最终目标是拥有一个简单的字典,其中的键都是小写的。

似乎应该有一些我可以覆盖的很小的原语集来使其工作,但根据我的所有研究和尝试,似乎事实并非如此:

  • 如果我覆盖__getitem__/__setitem__,则get/set不起作用。我该如何让它们工作?我肯定不需要单独实现它们?

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

  • 我需要reprupdate__init__吗?

  • 我是否应该只使用 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()

解决方案 1:

您可以使用模块中的ABC(抽象基类)dict轻松编写一个行为类似的对象。它甚至会告诉您是否遗漏了某个方法,因此下面是使 ABC 闭嘴的最小版本。collections.abc

from collections.abc import MutableMapping


class TransformedDict(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
# works too since we just use a normal dict
assert pickle.loads(pickle.dumps(s)) == s

我不会dict直接子类化(或其他内置类)。这通常毫无意义,因为您真正想要做的是实现 的接口dict。而这正是 ABC 的用途。

解决方案 2:

我怎样才能使 dict 的子类尽可能“完美”?

最终目标是拥有一个其中键都是小写的简单字典。

  • 如果我覆盖__getitem__/ __setitem__,则 get/set 不起作用。我该如何让它们工作?我肯定不需要单独实现它们?

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

  • 我需要 repr、update 和 吗__init__

  • 我应该只使用mutablemapping(似乎不应该使用UserDict
    DictMixin) 吗?如果是,该怎么做?文档并没有提供足够的启发。

可以接受的答案是我的第一种方法,但由于它存在一些问题,而且没有人解决过替代方案,实际上是子类化dict,所以我要在这里这样做。

接受的答案有什么问题?

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

我怎样才能使字典的子类尽可能“完美”?最终目标是拥有一个简单的字典,其中的键都是小写的。

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

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

理想情况下,任何类型检查代码都会测试我们期望的接口或抽象基类,但如果我们的数据对象被传递到正在测试的函数中dict- 而我们无法“修复”这些函数,那么该代码就会失败。

其他可能存在的疑问包括:

  • 接受的答案也缺少类方法:fromkeys

  • 接受的答案也有冗余__dict__- 因此占用了更多的内存空间:

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

实际上是子类化dict

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

如果我覆盖__getitem__/ __setitem__,则 get/set 不起作用。我该如何让它们工作?我肯定不需要单独实现它们?

好吧,单独实现它们是这种方法的缺点和使用的优点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__())

对于引用键的任何方法或特殊方法,我们都使用几乎样板化的方法,但除此之外,通过继承,我们可以免费获得方法:、、、、len和。虽然这需要仔细思考才能clear正确,但很容易看出这是可行的。items`keyspopitemvalues`

(请注意,haskey在 Python 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)

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

酸洗

并且 dict 子类 pickle 得很好:

>>> import pickle
>>> pickle.dumps(ld)
b'x80x03c__main__
LowerDict
qx00)x81qx01Xx03x00x00x00fooqx02Ns.'
>>> pickle.loads(pickle.dumps(ld))
{'foo': None}
>>> type(pickle.loads(pickle.dumps(ld)))
<class '__main__.LowerDict'>

__repr__

我需要 repr、update 和 吗__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
DictMixin) 吗?如果是,该怎么做?文档并没有提供足够的启发。

是的,这些代码又多了几行,但它们的目的是全面的。我的第一个倾向是使用公认的答案,如果有问题,我会看看我的答案——因为它有点复杂,而且没有 ABC 来帮助我正确设置界面。

过早优化是为了追求更高的性能而追求更高的复杂性。
MutableMapping更简单 - 因此在其他条件相同的情况下,它会立即获得优势。不过,为了列出所有差异,让我们进行比较和对比。

我应该补充一下,曾经有人推动将类似的词典放入模块中collections,但遭到了拒绝。您可能应该这样做:

my_dict[transform(key)]

它应该更容易调试。

比较和对比

有 6 个接口函数是用MutableMapping(缺少fromkeys)实现的,有 11 个是用dict子类实现的。我不需要实现__iter____len__,而是必须实现getsetdefault、、、和- 但这些相当简单,因为我pop可以对大多数实现使用继承。update`copy__contains__fromkeys`

在 Python 中实现MutableMapping的一些功能dict在 C 中实现 - 因此我希望dict子类在某些情况下性能更佳。

两种方法都得到了免费的__eq__- 这两种方法都仅在另一个字典全部为小写的情况下才假设相等 - 但是我再次认为dict子类的比较速度会更快。

概括:

  • 子类化MutableMapping更简单,出错的机会更少,但速度较慢,占用更多内存(参见冗余字典),并且会失败isinstance(x, dict)

  • 子类化dict速度更快,占用内存更少,并且可以通过isinstance(x, dict),但实现起来更复杂。

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

解决方案 3:

在尝试了前 两个建议之后,我决定采用 Python 2.7 的中间路线。也许 3 更明智,但对我来说:

class MyDict(MutableMapping):
   # ... the few __methods__ that mutablemapping requires
   # and then this monstrosity
   @property
   def __class__(self):
       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

只要你只需要在内部识别自己,这样就很难__am_i_me因为 python 的名称混淆而意外调用(这个名称会从该类之外的任何调用中重命名为)。在实践和文化上都比 s_MyDict__am_i_me更私密。_method

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


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

基本上:复制当前的 #2 选项,print 'method_name'向每个方法添加行,然后尝试这个并观察输出:

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

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

对于 来说,这是正确的MutableMapping,但是一旦你继承dict它,它就会变得无法控制。


编辑:作为更新,这已经运行了近两年,没有出现任何问题,在几十万行(呃,可能是几百万行)复杂、遗留的 Python 代码上。所以我对此非常满意 :)

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

解决方案 4:

我的要求稍微严格一些:

  • 我必须保留大小写信息(字符串是向用户显示的文件的路径,但它是一个 Windows 应用程序,因此内部所有操作都必须不区分大小写)

  • 我需要密钥尽可能小(这确实会对内存性能产生影响,从 370 mb 中砍掉了 110 mb)。这意味着缓存小写版本的密钥不是一种选择。

  • 我需要尽可能快地创建数据结构(这次又对性能和速度产生了影响)。我不得不使用内置

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

  • 事实证明很难做到这一点 - 请参阅:python 中不区分大小写的字符串类

  • 事实证明,显式的字典键处理会使代码变得冗长和混乱 - 并且容易出错(结构被传递到这里和那里,并且不清楚它们是否具有 CIStr 实例作为键/元素,容易忘记而且很some_dict[CIstr(path)]丑陋)

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

class CIstr(unicode):
    """See https://stackoverflow.com/a/43122305/281545, 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: https://stackoverflow.com/a/39375731/281545
    """
    __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__())

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

欢迎评论/更正:)

解决方案 5:

你所要做的就是

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

或者

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 中测试

解决方案 6:

collections.UserDict往往是最简单的选择,当你需要自定义dict

如另一个答案所示,正确覆盖非常棘手dict,而 则UserDict很容易。要回答原始问题,您可以获得具有较低键的字典:

import collections

class LowercaseDict(collections.UserDict):

  def __getitem__(self, key):
    return super().__getitem__(key.lower())

  def __setitem__(self, key, value):
    return super().__setitem__(key.lower(), value)

  def __delitem__(self, key):
    return super().__delitem__(key.lower())

  # Unfortunately, __contains__ is required currently due to
  # https://github.com/python/cpython/issues/91784
  def __contains__(self, key):
    return key.lower() in self.data


d = LowercaseDict(MY_KEY=0)  # Keys normalized in .__init__
d.update({'OTHER_KEY': 1})  # Keys normalized in .update
d['Hello'] = d['other_KEY']
assert 'HELLO' in d
print(d)  # All keys normalized {'my_key': 0, 'other_key': 1, 'hello': 1}

与 相反collections.abc.MutableMapping,您不需要__iter__,,,__len__......__init__子类化UserDict要容易得多。

但是UserDictMutableMapping,而不是dict,因此:

assert not isinstance(collections.UserDict(), dict)
assert isinstance(collections.UserDict(), collections.abc.MutableMapping)
相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1008  
  在项目管理中,变更是一个不可避免的现象。无论是客户需求的调整、市场环境的变化,还是技术方案的更新,都可能引发项目的变更。如果处理不当,这些变更可能会导致项目延期、成本超支,甚至项目失败。因此,如何有效地应对项目变更,成为项目管理中的核心挑战之一。IPD(集成产品开发)作为一种高效的项目管理方法,其流程图不仅能够帮助团队...
IPD流程中的charter   0  
  IPD(Integrated Product Development,集成产品开发)是华为在长期实践中总结出的一套高效产品开发管理体系。它不仅帮助华为在全球市场中脱颖而出,也成为许多企业提升产品开发效率的参考标杆。IPD的核心在于通过跨部门协作、流程优化和资源整合,实现从需求分析到产品交付的全生命周期管理。通过实施IP...
IPD开发流程管理   0  
  华为IPD(集成产品开发)流程是一种以客户需求为导向、跨部门协同的高效项目管理方法。它通过系统化的流程设计和严格的阶段控制,确保项目从概念到交付的每个环节都能高效运作。IPD流程的核心在于打破传统职能部门的壁垒,将产品开发、市场、销售、供应链等关键环节整合到一个统一的框架中,从而实现资源的优化配置和信息的无缝流动。这种...
IPD流程中TR   0  
  在项目管理的实践中,CDCP(Certified Data Center Professional)认证评审是一个至关重要的环节。通过这一评审,项目团队不仅能够验证其数据中心设计和运营的合规性,还能提升整体管理水平。为了确保评审的顺利进行,准备一系列关键文档是必不可少的。这些文档不仅是评审的依据,也是项目团队与评审专家...
华为IPD是什么   0  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用