自动使类可散列

Posted

技术标签:

【中文标题】自动使类可散列【英文标题】:Automatically making a class hashable 【发布时间】:2012-09-12 19:30:14 【问题描述】:

有几种标准方法可以使类可散列,例如(借用SO):

# assume X has 2 attributes: attr_a and attr_b
class X:
  def __key(self):
    return (self.attr_a, self.attr_b)

  def __eq__(x, y):
    return isinstance(y, x.__class__) and x.__key() == y.__key()

  def __hash__(self):
    return hash(self.__key())

现在假设我有很多类要设为可散列。它们都是不可变的,具有不可变的属性,并且批量散列所有这些属性是可以接受的(对于具有太多属性的类,我们只想散列几个足以避免大多数冲突的属性)。我可以避免为每个类手动编写__key() 方法吗?

为它们创建一个定义__key()__eq____hash__ 的基类是否是个好主意?特别是,我不确定找到应该进入__hash__ 的所有实例属性是否可行。我知道这是generally impossible,但在这种情况下,我们可以对对象进行更多假设(例如,它是不可变的 - 在__init__ 完成后,它的属性都是可散列的,等等)。

(如果继承层次不起作用,也许装饰器会起作用?)

【问题讨论】:

继承对我来说很好...... 【参考方案1】:

实例将其属性存储在self.__dict__

>>> class Foo(object):
...     def __init__(self, foo='bar', spam='eggs'):
...         self.foo = foo
...         self.spam = spam
... 
>>> f = Foo()
>>> f.__dict__
'foo': 'bar', 'spam': 'eggs'

如果您没有在实例上存储任何方法,则默认的 .__key() 可能是:

def __key(self):
    return tuple(v for k, v in sorted(self.__dict__.items()))

我们按属性名称对项目进行排序; tuple() 调用确保我们返回适合hash() 调用的不可变序列。

对于更复杂的设置,您必须测试values() 返回的类型(跳过函数等)或使用特定的属性模式或重新调整__slots__ 的用途以列出您可以使用的适当属性。

与您的 __hash____eq__ 方法一起,这将为您的所有不可变类创建一个很好的基类。

【讨论】:

可以__key 只返回self.__dict__.values(),而不转换为tuple?视图对象似乎是可散列的。 我不需要确保self.__dict__.values()中的属性顺序是一致的吗?由于我的__key 不仅用于散列,还用于__eq__,所以我不能承受__key() 为两个不相等的实例返回相同值的风险。而且我认为如果字典以相同的顺序创建,则不能保证字典以相同的顺序迭代。 (实际上,它可能不会以相同的顺序创建...__init__ 在某些情况下可能会遵循不同的分支,从而导致不同的属性分配顺序。) 我已经添加了排序,以确保稳定排序。 @max -- 创建字典后,只要您不添加更多键/值,它就会以相同的顺序(针对该会话)进行迭代。现在pypy 使用的顺序可能与Cpython 不同,或者使用 python2.6 和 python2.7 可能不同——但这并不重要...... 但是当我测试x.__key() == y.__key()(是否相等)时,我将比较两个不同 __dict__ 对象,而不是同一个对象,因为每个实例都有自己的@ 987654343@。我很好奇,这种保证是否扩展到同一会话中的不同词典,其中插入顺序相同?无论如何,我不能保证这里的插入顺序是一样的。【参考方案2】:

如果你为你的属性假设约定,你就可以做到这一点。在你的例子中, 这将是微不足道的,因为您的属性以“attr_”开头-因此您可以将 __key 方法编写为:

def __key(self):
    return tuple (getattr(self, attr) for attr in self.__dict__ if attr.startswith("attr_") )

如您所见 - 您可以找到任何对生成器表达式的过滤条件进行测试的测试都将满足您的需求。

我可以给你的一个建议是让你的类使用 Python 的 __slots__ 功能: 这不仅会使您的属性名称易于查找,而且会使您的不可变对象更高效地使用并且占用更少的内存。

class X:
    __slots__ = ("a", "b", "c")
    def __key(self):
        return tuple (getattr(self, attr) for attr in self.__class__.__slots__ )

编辑 回答 O.P. 的第一条评论:

当然,这适用于继承。如果您将始终为它们使用所有对象的属性,则不需要表达式的“if”部分 - 将函数编写为_key(而不是__key,它在内部为每个类创建一个唯一的名称)在您的层次结构顶部的一个类上,它将适用于您的所有类。

【讨论】:

如果你要基于__dict__,为什么不直接使用__dict__.items()__dict__.values() 并直接从实例字典中获取属性值? 谢谢。不幸的是,我正在使用现有的代码体。我希望在不对它们进行太多更改的情况下使某些类可散列。我想这是可行的,但有点耗时。

以上是关于自动使类可散列的主要内容,如果未能解决你的问题,请参考以下文章

使 python 用户定义的类可排序、可散列

如何在不使它们不可变的情况下使 python 数据类可散列?

使类可迭代尊重继承

如何使类可迭代?

在 Boost.MPI 中使类可序列化是啥意思?

列表不可散列,但元组可散列?