如何为namedtuple子类提供额外初始化?

59 投票
3 回答
29413 浏览
提问于 2025-04-16 03:37

假设我有一个叫做 namedtuple 的东西,长得像这样:

EdgeBase = namedtuple("EdgeBase", "left, right")

我想为这个东西实现一个自定义的哈希函数,所以我创建了一个子类,代码如下:

class Edge(EdgeBase):
    def __hash__(self):
        return hash(self.left) * hash(self.right)

因为这个对象是不可变的,我希望哈希值只计算一次,所以我这样做:

class Edge(EdgeBase):
    def __init__(self, left, right):
        self._hash = hash(self.left) * hash(self.right)

    def __hash__(self):
        return self._hash

看起来这个方法是有效的,但我对在Python中进行子类化和初始化的理解不是很清楚,尤其是关于元组的部分。这个解决方案有没有什么潜在的问题?有没有推荐的做法?这样做可以吗?提前谢谢大家。

3 个回答

4

这个问题中的代码如果在多重继承的情况下被子类化,最好在__init__里加个超级调用(super call),不过除此之外,代码是正确的。

class Edge(EdgeBase):
    def __init__(self, left, right):
        super(Edge, self).__init__(left, right)
        self._hash = hash(self.left) * hash(self.right)

    def __hash__(self):
        return self._hash

虽然元组(tuples)是只读的,但它们子类中的元组部分才是只读的,其他属性还是可以正常写入的。这就是为什么无论是在__init__还是__new__中都能给_hash赋值的原因。如果你想让子类完全只读,可以把它的__slots__设置为(),这样还可以节省内存,但那样的话你就不能给_hash赋值了。

5

在Python 3.7及以上版本中,你可以使用数据类来轻松创建可以被哈希的类。

代码

假设我们有两个整数类型的变量,leftright,我们可以通过unsafe_hash+这个关键词来使用默认的哈希方式:

import dataclasses as dc


@dc.dataclass(unsafe_hash=True)
class Edge:
    left: int
    right: int


hash(Edge(1, 2))
# 3713081631934410656

现在,我们可以把这些(可变的)可哈希对象用作集合中的元素或者字典中的键。

{Edge(1, 2), Edge(1, 2), Edge(2, 1), Edge(2, 3)}
# {Edge(left=1, right=2), Edge(left=2, right=1), Edge(left=2, right=3)}

详细信息

我们也可以选择重写__hash__这个函数:

@dc.dataclass
class Edge:
    left: int
    right: int

    def __post_init__(self):
        # Add custom hashing function here
        self._hash = hash((self.left, self.right))         # emulates default

    def __hash__(self):
        return self._hash


hash(Edge(1, 2))
# 3713081631934410656

进一步解释一下@ShadowRanger的评论,提问者自定义的哈希函数并不可靠。特别是,属性值可能会互换,比如hash(Edge(1, 2)) == hash(Edge(2, 1)),这可能不是他们想要的结果。

+注意,"unsafe"这个名字意味着尽管是可变对象,默认的哈希仍然会被使用。这在字典中使用不可变键时可能会导致问题。如果需要不可变的哈希,可以通过适当的关键词来开启。更多关于哈希逻辑的信息,以及一个相关问题

59

2017年的更新: 结果发现,namedtuple并不是个好主意。现在有个更现代的选择叫attrs

class Edge(EdgeBase):
    def __new__(cls, left, right):
        self = super(Edge, cls).__new__(cls, left, right)
        self._hash = hash(self.left) * hash(self.right)
        return self

    def __hash__(self):
        return self._hash

这里你需要用到__new__,因为元组是不可变的。不可变的对象是在__new__里创建的,然后返回给用户,而不是在__init__里填充数据。

在调用super__new__时,cls需要传递两次,因为__new__出于一些历史原因,实际上是一个staticmethod

撰写回答