【问题标题】:Python hashable dictsPython 可散列的字典
【发布时间】:2010-11-12 04:57:44
【问题描述】:

作为一个练习,主要是为了我自己的娱乐,我正在实现一个回溯 Packrat 解析器。这样做的灵感是我想更好地了解卫生宏如何在类似 algol 的语言中工作(与您通常在其中找到的无语法 lisp 方言相对)。正因为如此,不同的通过输入可能会看到不同的语法,因此缓存的解析结果是无效的,除非我还将语法的当前版本与缓存的解析结果一起存储。 (编辑:使用键值集合的结果是它们应该是不可变的,但我不打算公开接口以允许它们被更改,所以无论是可变集合还是不可变集合没问题)

问题是 python dicts 不能作为其他 dicts 的键出现。即使使用元组(反正我会这样做)也无济于事。

>>> cache = {}
>>> rule = {"foo":"bar"}
>>> cache[(rule, "baz")] = "quux"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
>>> 

我猜它必须是元组。现在 python 标准库提供了我需要的大致内容,collections.namedtuple 有一个非常不同的语法,但 可以 用作键。从上面的会话继续:

>>> from collections import namedtuple
>>> Rule = namedtuple("Rule",rule.keys())
>>> cache[(Rule(**rule), "baz")] = "quux"
>>> cache
{(Rule(foo='bar'), 'baz'): 'quux'}

好的。但是我必须为我想使用的规则中的每个可能的键组合创建一个类,这还不错,因为每个解析规则都知道它使用什么参数,因此可以同时定义该类作为解析规则的函数。

编辑:namedtuples 的另一个问题是它们是严格定位的。看起来应该不同的两个元组实际上可以是相同的:

>>> you = namedtuple("foo",["bar","baz"])
>>> me = namedtuple("foo",["bar","quux"])
>>> you(bar=1,baz=2) == me(bar=1,quux=2)
True
>>> bob = namedtuple("foo",["baz","bar"])
>>> you(bar=1,baz=2) == bob(bar=1,baz=2)
False

tl'dr:我如何获得可以用作其他dicts 的密钥的dicts?

对答案有所了解后,这是我正在使用的更完整的解决方案。请注意,这做了一些额外的工作,以使生成的 dicts 出于实际目的模糊地不可变。当然,通过调用dict.__setitem__(instance, key, value) 来破解它仍然很容易,但我们都是成年人。

class hashdict(dict):
    """
    hashable dict implementation, suitable for use as a key into
    other dicts.

        >>> h1 = hashdict({"apples": 1, "bananas":2})
        >>> h2 = hashdict({"bananas": 3, "mangoes": 5})
        >>> h1+h2
        hashdict(apples=1, bananas=3, mangoes=5)
        >>> d1 = {}
        >>> d1[h1] = "salad"
        >>> d1[h1]
        'salad'
        >>> d1[h2]
        Traceback (most recent call last):
        ...
        KeyError: hashdict(bananas=3, mangoes=5)

    based on answers from
       http://stackoverflow.com/questions/1151658/python-hashable-dicts

    """
    def __key(self):
        return tuple(sorted(self.items()))
    def __repr__(self):
        return "{0}({1})".format(self.__class__.__name__,
            ", ".join("{0}={1}".format(
                    str(i[0]),repr(i[1])) for i in self.__key()))

    def __hash__(self):
        return hash(self.__key())
    def __setitem__(self, key, value):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def __delitem__(self, key):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def clear(self):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def pop(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def popitem(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def setdefault(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def update(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    # update is not ok because it mutates the object
    # __add__ is ok because it creates a new object
    # while the new object is under construction, it's ok to mutate it
    def __add__(self, right):
        result = hashdict(self)
        dict.update(result, right)
        return result

if __name__ == "__main__":
    import doctest
    doctest.testmod()

【问题讨论】:

  • hashdict 必须是不可变的,至少在您开始对其进行哈希处理之后,那么为什么不将keyhash 值缓存为hashdict 对象的属性呢?我修改了__key()__hash__(),并进行了测试,确认它快得多。 SO不允许在cmets中格式化代码,所以我将它链接在这里:sam.aiki.info/hashdict.py
  • 我不知道这是否太简单了,但是用str() 散列字典可能有用吗? IE。在您的示例中:cache[(str(rule), "baz")] = "quux"

标签: python


【解决方案1】:

这是制作可散列字典的简单方法。请记住不要因为明显的原因在嵌入另一个字典后对其进行变异。

class hashabledict(dict):
    def __hash__(self):
        return hash(tuple(sorted(self.items())))

【讨论】:

  • 这并不能明显确保 eqhash 的一致性,而我之前的回答是通过使用 __key 方法(实际上任何一种方法都应该工作,虽然这个可能会通过制作一个不需要的迭代列表来减慢 - 可以通过 s/items/iteritems/ 修复 - 假设 Python 2.* 正如你没有说的那样;-)。
  • 只使用frozenset而不是使用排序的元组可能会更好。这不仅会更快,而且您不能假设字典键具有可比性。
  • 似乎应该有一种方法可以避免O(n*log(n)) 的哈希函数,其中ndict 条目的数量。有谁知道 Python 的 frozenset 哈希函数是否在线性时间内运行?
  • @HelloGoodbye 也可以像 dict(key1=value1, key2=value2,...)dict([(key1, value1), (key2, value2),...)]) 一样创建字典。这同样适用于这一点。您发布的创作称为literal
  • @smido:谢谢。我还发现您可以只转换文字,即hashabledict({key_a: val_a, key_b: val_b, ...})
【解决方案2】:

Hashables 应该是不可变的 - 不强制执行此操作,但相信您不会在 dict 首次用作键后对其进行变异,以下方法可行:

class hashabledict(dict):
  def __key(self):
    return tuple((k,self[k]) for k in sorted(self))
  def __hash__(self):
    return hash(self.__key())
  def __eq__(self, other):
    return self.__key() == other.__key()

如果您确实需要改变您的 dicts 并且仍然想将它们用作键,那么复杂性会爆炸成百倍 - 并不是说​​它无法完成,但我会等到一个非常具体的指示后再进入令人难以置信的沼泽!-)

【讨论】:

  • 我当然不想在准备好字典后对其进行变异。这将使 packrad 算法的其余部分崩溃。
  • 然后我建议的子类将起作用 - 请注意它如何绕过“位置”问题(之前您已编辑您的问题以指出它;-) 987654325@ 在 __key;-)。
  • namedtuple 的位置依赖行为让我大吃一惊。我一直在玩它,认为它可能仍然是解决问题的一种更简单的方法,但这几乎使我所有的希望破灭(并且需要回滚:()
  • 假设我有一个字典,我想将它转换为一个 hashabledict。我该怎么做?
【解决方案3】:

使字典可用于您的目的所需要做的就是添加一个 __hash__ 方法:

class Hashabledict(dict):
    def __hash__(self):
        return hash(frozenset(self))

注意,frozenset 转换适用于所有字典(即它不需要键是可排序的)。同样,对字典值也没有限制。

如果有许多具有相同键但具有不同值的字典,则有必要让哈希值考虑这些值。最快的方法是:

class Hashabledict(dict):
    def __hash__(self):
        return hash((frozenset(self), frozenset(self.itervalues())))

这比frozenset(self.iteritems()) 更快有两个原因。首先,frozenset(self) 步骤重用了存储在字典中的哈希值,节省了对hash(key) 的不必要调用。其次,使用 itervalues 将直接访问值,并避免每次查找时 items 使用的许多内存分配器调用在内存中形成新的许多键/值元组。

【讨论】:

  • @RaymondHettinger 如果我错了,请纠正我,但我认为dict 本身不会缓存其键的哈希值 - 尽管个别类(如str)可能并且确实选择缓存他们的哈希。至少当我使用自定义类实例作为键创建dict 时,每个访问操作(python 3.4)都会调用它们的__hash__ 方法。无论我是否正确,我不确定hash(frozenset(self)) 如何重用预先计算的哈希值,除非它们被缓存在键本身中(在这种情况下,hash(frozenset(self.items()) 也会重用它们)。
  • 关于(键/值)元组创建的第二点,我认为 .items() 方法返回一个视图而不是元组列表,并且该视图的创建不涉及复制底层的键和值。 (再次是 Python 3.4。)也就是说,如果大多数输入具有不同的键,我确实看到了仅对键进行散列的优势 - 因为(1)散列值非常昂贵,并且(2)要求值是可散列的非常严格
  • 这也有可能为两个不同的字典创建相同的哈希。考虑{'one': 1, 'two': 2}{'one': 2, 'two': 1}
  • Mike Graham 在他的comment 中指出除了定义__missing__ 以外的任何其他原因派生dict 都是一个坏主意。 你怎么看?
  • 自 Python 2.2 以来,从 dict 继承的子类已经得到很好的定义。有关 Python 标准库的示例,请参阅 collections.OrderedDict 和 collections.Counter。另一条评论基于这样一种毫无根据的信念,即只有 MutableMapping 的子类是明确定义的。
【解决方案4】:

给出的答案还可以,但可以通过使用frozenset(...) 而不是tuple(sorted(...)) 来生成哈希来改进:

>>> import timeit
>>> timeit.timeit('hash(tuple(sorted(d.iteritems())))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')")
4.7758948802947998
>>> timeit.timeit('hash(frozenset(d.iteritems()))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')")
1.8153600692749023

性能优势取决于字典的内容,但在我测试过的大多数情况下,使用frozenset 进行散列至少快2倍(主要是因为它不需要排序)。

【讨论】:

  • 注意,不需要同时包含键和值。这个解决方案会快得多hash(frozenset(d)).
  • @RaymondHettinger:hash(frozenset(d)) 导致 2 个具有相似键但值不同的字典的相同哈希!
  • 这不是问题。 __eq__ 的工作是区分不同值的字典。 __hash__ 的工作只是减少搜索空间。
  • 这对于哈希和映射的理论概念是正确的,但对于使用字典作为查找的缓存来说并不实用——具有相似键但不同值的字典传递给内存缓存函数的情况并不少见。在这种情况下,如果只使用键来构建哈希,缓存实际上会变成列表而不是映射。
  • 在具有相同键和不同值的字典的特殊情况下,您最好只存储基于frozenset(d.itervalues()) 的散列。在 dicts 具有不同键的情况下,frozenset(d) 更快 并且对键的哈希性没有限制。最后,请记住 dict.__eq__ 方法将更快地检查相等的键/值对,任何东西都可以计算所有键/值对元组的哈希值。使用键/值元组也是有问题的,因为它会丢弃所有键的存储哈希(这就是frozenset(d) 如此之快的原因)。
【解决方案5】:

一个相当干净、直接的实现是

import collections

class FrozenDict(collections.Mapping):
    """Don't forget the docstrings!!"""

    def __init__(self, *args, **kwargs):
        self._d = dict(*args, **kwargs)

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

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

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

    def __hash__(self):
        return hash(tuple(sorted(self._d.iteritems())))

【讨论】:

  • 为何如此合理、干净、直截了当? IE。请解释与其他答案的差异,例如__iter____len__ 的必要性。
  • @KarlRichter,我从来没有说过这是合理的,只是相当干净。 ;)
  • @KarlRichter,我定义了__iter____len__,因为我必须定义collections.Mapping;如何使用collections.Mapping 在集合模块文档中有很好的介绍。其他人觉得没有必要,因为他们正在派生dict。除了定义__missing__ 以外的任何其他原因派生dict 是一个坏主意。 dict 规范没有说明 dict 在这种情况下是如何工作的,实际上这最终会产生大量通常不太有用的非虚拟方法,并且在这种特殊情况下将具有不相关行为的残留方法。跨度>
【解决方案6】:

我不断回到这个话题......这是另一个变体。我对子类化dict 来添加__hash__ 方法感到不安; dict 是可变的这个问题几乎无法逃脱,并且相信它们不会改变似乎是一个软弱的想法。因此,我转而考虑基于本身不可变的内置类型构建映射。尽管tuple 是一个显而易见的选择,但访问其中的值意味着排序和平分;不是问题,但它似乎并没有充分利用它所构建类型的强大功能。

如果您将键、值对塞入frozenset 会怎样?这需要什么,它是如何工作的?

第 1 部分,您需要一种对 'item' 进行编码的方式,以使 freezeset 主要通过它们的键来处理它们;我会为此创建一个小子类。

import collections
class pair(collections.namedtuple('pair_base', 'key value')):
    def __hash__(self):
        return hash((self.key, None))
    def __eq__(self, other):
        if type(self) != type(other):
            return NotImplemented
        return self.key == other.key
    def __repr__(self):
        return repr((self.key, self.value))

仅此一项就让您陷入不可变映射的距离:

>>> frozenset(pair(k, v) for k, v in enumerate('abcd'))
frozenset([(0, 'a'), (2, 'c'), (1, 'b'), (3, 'd')])
>>> pairs = frozenset(pair(k, v) for k, v in enumerate('abcd'))
>>> pair(2, None) in pairs
True
>>> pair(5, None) in pairs
False
>>> goal = frozenset((pair(2, None),))
>>> pairs & goal
frozenset([(2, None)])

D'oh! 不幸的是,当你使用集合运算符时,元素相等但不是同一个对象;返回值中的哪一个是undefined,我们将不得不经历更多的回旋。

>>> pairs - (pairs - goal)
frozenset([(2, 'c')])
>>> iter(pairs - (pairs - goal)).next().value
'c'

但是,以这种方式查找值很麻烦,而且更糟糕的是,会创建大量中间集;那不行!我们将创建一个“假”键值对来绕过它:

class Thief(object):
    def __init__(self, key):
        self.key = key
    def __hash__(self):
        return hash(pair(self.key, None))
    def __eq__(self, other):
        self.value = other.value
        return pair(self.key, None) == other

这导致问题较少:

>>> thief = Thief(2)
>>> thief in pairs
True
>>> thief.value
'c'

这就是所有深奥的魔法;剩下的就是把它全部包装成一个像字典一样具有 interface 的东西。因为我们是从frozenset 继承的,它有一个非常不同的接口,所以有很多方法;我们从 collections.Mapping 那里得到了一点帮助,但大部分工作是覆盖 frozenset 方法,用于类似 dicts 的版本,而不是:

class FrozenDict(frozenset, collections.Mapping):
    def __new__(cls, seq=()):
        return frozenset.__new__(cls, (pair(k, v) for k, v in seq))
    def __getitem__(self, key):
        thief = Thief(key)
        if frozenset.__contains__(self, thief):
            return thief.value
        raise KeyError(key)
    def __eq__(self, other):
        if not isinstance(other, FrozenDict):
            return dict(self.iteritems()) == other
        if len(self) != len(other):
            return False
        for key, value in self.iteritems():
            try:
                if value != other[key]:
                    return False
            except KeyError:
                return False
        return True
    def __hash__(self):
        return hash(frozenset(self.iteritems()))
    def get(self, key, default=None):
        thief = Thief(key)
        if frozenset.__contains__(self, thief):
            return thief.value
        return default
    def __iter__(self):
        for item in frozenset.__iter__(self):
            yield item.key
    def iteritems(self):
        for item in frozenset.__iter__(self):
            yield (item.key, item.value)
    def iterkeys(self):
        for item in frozenset.__iter__(self):
            yield item.key
    def itervalues(self):
        for item in frozenset.__iter__(self):
            yield item.value
    def __contains__(self, key):
        return frozenset.__contains__(self, pair(key, None))
    has_key = __contains__
    def __repr__(self):
        return type(self).__name__ + (', '.join(repr(item) for item in self.iteritems())).join('()')
    @classmethod
    def fromkeys(cls, keys, value=None):
        return cls((key, value) for key in keys)

这最终确实回答了我自己的问题:

>>> myDict = {}
>>> myDict[FrozenDict(enumerate('ab'))] = 5
>>> FrozenDict(enumerate('ab')) in myDict
True
>>> FrozenDict(enumerate('bc')) in myDict
False
>>> FrozenDict(enumerate('ab', 3)) in myDict
False
>>> myDict[FrozenDict(enumerate('ab'))]
5

【讨论】:

    【解决方案7】:

    @Unknown 接受的答案以及@AlexMartelli 的答案都可以正常工作,但仅限于以下限制:

    1. 字典的值必须是可散列的。例如,hash(hashabledict({'a':[1,2]})) 将引发 TypeError
    2. 键必须支持比较操作。例如,hash(hashabledict({'a':'a', 1:1})) 将引发 TypeError
    3. 键上的比较运算符强制执行总排序。例如,如果字典中的两个键是 frozenset((1,2,3))frozenset((4,5,6)),则它们在两个方向上比较不相等。因此,使用此类键对字典中的项目进行排序可能会导致任意顺序,因此会违反相等对象必须具有相同哈希值的规则。

    @ObenSonne 更快的回答解除了约束 2 和 3,但仍受约束 1 的约束(值必须是可散列的)。

    @RaymondHettinger 更快的回答解除了所有 3 个约束,因为它在哈希计算中不包括 .values()。但是,它的性能只有在以下情况下才是好的:

    1. 大多数需要散列的(不相等的)字典有不完全相同的.keys()

    如果不满足这个条件,哈希函数仍然有效,但可能会导致太多的冲突。例如,在所有字典都是从网站模板生成的极端情况下(字段名称作为键,用户输入作为值),键将始终相同,哈希函数将为所有输入返回相同的值.因此,依赖这种哈希函数的哈希表在检索项目时会变得像列表一样慢(O(N) 而不是O(1))。

    我认为即使我上面列出的所有 4 个约束都被违反,以下解决方案也能很好地工作。它还有一个额外的优势,它不仅可以散列字典,还可以散列任何容器,即使它们具有嵌套的可变容器。

    我非常感谢您对此的任何反馈,因为到目前为止我只对此进行了轻微的测试。

    # python 3.4
    import collections
    import operator
    import sys
    import itertools
    import reprlib
    
    # a wrapper to make an object hashable, while preserving equality
    class AutoHash:
        # for each known container type, we can optionally provide a tuple
        # specifying: type, transform, aggregator
        # even immutable types need to be included, since their items
        # may make them unhashable
    
        # transformation may be used to enforce the desired iteration
        # the result of a transformation must be an iterable
        # default: no change; for dictionaries, we use .items() to see values
    
        # usually transformation choice only affects efficiency, not correctness
    
        # aggregator is the function that combines all items into one object
        # default: frozenset; for ordered containers, we can use tuple
    
        # aggregator choice affects both efficiency and correctness
        # e.g., using a tuple aggregator for a set is incorrect,
        # since identical sets may end up with different hash values
        # frozenset is safe since at worst it just causes more collisions
        # unfortunately, no collections.ABC class is available that helps
        # distinguish ordered from unordered containers
        # so we need to just list them out manually as needed
    
        type_info = collections.namedtuple(
            'type_info',
            'type transformation aggregator')
    
        ident = lambda x: x
        # order matters; first match is used to handle a datatype
        known_types = (
            # dict also handles defaultdict
            type_info(dict, lambda d: d.items(), frozenset), 
            # no need to include set and frozenset, since they are fine with defaults
            type_info(collections.OrderedDict, ident, tuple),
            type_info(list, ident, tuple),
            type_info(tuple, ident, tuple),
            type_info(collections.deque, ident, tuple),
            type_info(collections.Iterable, ident, frozenset) # other iterables
        )
    
        # hash_func can be set to replace the built-in hash function
        # cache can be turned on; if it is, cycles will be detected,
        # otherwise cycles in a data structure will cause failure
        def __init__(self, data, hash_func=hash, cache=False, verbose=False):
            self._data=data
            self.hash_func=hash_func
            self.verbose=verbose
            self.cache=cache
            # cache objects' hashes for performance and to deal with cycles
            if self.cache:
                self.seen={}
    
        def hash_ex(self, o):
            # note: isinstance(o, Hashable) won't check inner types
            try:
                if self.verbose:
                    print(type(o),
                        reprlib.repr(o),
                        self.hash_func(o),
                        file=sys.stderr)
                return self.hash_func(o)
            except TypeError:
                pass
    
            # we let built-in hash decide if the hash value is worth caching
            # so we don't cache the built-in hash results
            if self.cache and id(o) in self.seen:
                return self.seen[id(o)][0] # found in cache
    
            # check if o can be handled by decomposing it into components
            for typ, transformation, aggregator in AutoHash.known_types:
                if isinstance(o, typ):
                    # another option is:
                    # result = reduce(operator.xor, map(_hash_ex, handler(o)))
                    # but collisions are more likely with xor than with frozenset
                    # e.g. hash_ex([1,2,3,4])==0 with xor
    
                    try:
                        # try to frozenset the actual components, it's faster
                        h = self.hash_func(aggregator(transformation(o)))
                    except TypeError:
                        # components not hashable with built-in;
                        # apply our extended hash function to them
                        h = self.hash_func(aggregator(map(self.hash_ex, transformation(o))))
                    if self.cache:
                        # storing the object too, otherwise memory location will be reused
                        self.seen[id(o)] = (h, o)
                    if self.verbose:
                        print(type(o), reprlib.repr(o), h, file=sys.stderr)
                    return h
    
            raise TypeError('Object {} of type {} not hashable'.format(repr(o), type(o)))
    
        def __hash__(self):
            return self.hash_ex(self._data)
    
        def __eq__(self, other):
            # short circuit to save time
            if self is other:
                return True
    
            # 1) type(self) a proper subclass of type(other) => self.__eq__ will be called first
            # 2) any other situation => lhs.__eq__ will be called first
    
            # case 1. one side is a subclass of the other, and AutoHash.__eq__ is not overridden in either
            # => the subclass instance's __eq__ is called first, and we should compare self._data and other._data
            # case 2. neither side is a subclass of the other; self is lhs
            # => we can't compare to another type; we should let the other side decide what to do, return NotImplemented
            # case 3. neither side is a subclass of the other; self is rhs
            # => we can't compare to another type, and the other side already tried and failed;
            # we should return False, but NotImplemented will have the same effect
            # any other case: we won't reach the __eq__ code in this class, no need to worry about it
    
            if isinstance(self, type(other)): # identifies case 1
                return self._data == other._data
            else: # identifies cases 2 and 3
                return NotImplemented
    
    d1 = {'a':[1,2], 2:{3:4}}
    print(hash(AutoHash(d1, cache=True, verbose=True)))
    
    d = AutoHash(dict(a=1, b=2, c=3, d=[4,5,6,7], e='a string of chars'),cache=True, verbose=True)
    print(hash(d))
    

    【讨论】:

      【解决方案8】:

      您可能还想添加这两种方法以使 v2 酸洗协议与 hashdict 实例一起工作。否则 cPickle 将尝试使用 hashdict.____setitem____ 导致 TypeError。有趣的是,使用其他两个版本的协议,您的代码可以正常工作。

      def __setstate__(self, objstate):
          for k,v in objstate.items():
              dict.__setitem__(self,k,v)
      def __reduce__(self):
          return (hashdict, (), dict(self),)
      

      【讨论】:

        【解决方案9】:

        如果您不将数字放入字典中并且永远不会丢失包含字典的变量,则可以这样做:

        cache[id(rule)] = "whatever"

        因为 id() 对于每个字典都是唯一的

        编辑:

        哦,对不起,是的,在这种情况下,其他人说的会更好。我认为您也可以将字典序列化为字符串,例如

        cache[ 'foo:bar' ] = 'baz'

        如果你需要从密钥中恢复你的字典,那么你必须做一些更丑陋的事情,比如

        cache[ 'foo:bar' ] = ( {'foo':'bar'}, 'baz' )

        我想这样做的好处是您不必编写那么多代码。

        【讨论】:

        • 嗯,不;这不是我想要的:cache[id({'foo':'bar'})] = 'baz'; id({'foo':'bar'}) not in cache,当我想首先使用字典作为键时,能够动态创建键很重要。
        • 序列化字典可能没问题,你有关于序列化它们的建议吗?这就是我要找的。​​span>
        猜你喜欢
        • 2013-12-05
        • 2012-01-21
        • 2012-04-20
        • 1970-01-01
        • 2011-08-18
        • 2021-11-21
        • 2020-09-05
        • 1970-01-01
        • 2019-08-29
        相关资源
        最近更新 更多