如何为namedtuple子类提供额外初始化?
假设我有一个叫做 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 个回答
这个问题中的代码如果在多重继承的情况下被子类化,最好在__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赋值了。
在Python 3.7及以上版本中,你可以使用数据类来轻松创建可以被哈希的类。
代码
假设我们有两个整数类型的变量,left
和right
,我们可以通过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"这个名字意味着尽管是可变对象,默认的哈希仍然会被使用。这在字典中使用不可变键时可能会导致问题。如果需要不可变的哈希,可以通过适当的关键词来开启。更多关于哈希逻辑的信息,以及一个相关问题。
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
。