ti-enxame.com

Como fornecer uma assinatura adicional para uma subclasse de namedtuple?

Suponha que eu tenha um namedtuple assim:

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

Eu quero implementar uma função hash personalizada para isso, então eu crio a seguinte subclasse:

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

Como o objeto é imutável, quero que o valor de hash seja calculado apenas uma vez, portanto, faço isso:

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

    def __hash__(self):
        return self._hash

Isto parece estar funcionando, mas eu realmente não tenho certeza sobre subclassificação e inicialização em Python, especialmente com tuplas. Há alguma armadilha para esta solução? Existe uma maneira recomendada de como fazer isso? Está bom? Desde já, obrigado.

44
Björn Pollex

editar para 2017: acontece que namedtuple não é uma ótima idéia . attrs é a alternativa moderna.

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__ é o que você quer chamar aqui porque as tuplas são imutáveis. Objetos imutáveis ​​são criados em __new__ e depois retornados ao usuário, em vez de serem preenchidos com dados em __init__.

cls tem que ser passado duas vezes para a chamada super em __new__ porque __new__ é, por razões históricas/ímpares implicitamente uma staticmethod.

48
habnabit

O código na questão pode se beneficiar de uma super chamada no __init__ caso ele tenha uma subclasse em uma situação de herança múltipla, mas caso contrário está correto.

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

Enquanto as tuplas são somente de leitura, somente as partes Tuple de suas subclasses são readonly, outras propriedades podem ser escritas como de costume, o que é o que permite a atribuição de _hash, independentemente de ser feito em __init__ ou __new__. Você pode fazer a subclasse totalmente somente leitura configurando __slots__ para (), que tem o benefício adicional de salvar memória, mas você não poderá atribuir a _hash.

3
Gordon Wrigley

No Python 3.7+, agora você pode usar dataclasses para criar classes hashable com facilidade.

código

Assumindo int tipos de left e right, usamos o hashing padrão via unsafe_hash+ palavra chave:

import dataclasses as dc


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


hash(Edge(1, 2))
# 3713081631934410656

Agora podemos usar esses objetos (mutáveis) hashable como elementos em um conjunto ou (chaves em um dict).

{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)}

Detalhes

Podemos alternativamente substituir a função __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

Expandindo o comentário do @ ShadowRanger, a função hash personalizada do OP não é confiável. Em particular, os valores de atributos podem ser trocados, por ex. hash(Edge(1, 2)) == hash(Edge(2, 1)), o que provavelmente não é o que se pretende.

+Note que o nome "inseguro" sugere que o hash padrão será usado apesar do objeto ser mutável. Isso pode ser indesejado, particularmente em dit, esperando chaves imutáveis. O hashing imutável pode ser ativado com as palavras-chave apropriadas. Veja também mais em lógica de hash em dataclasses e um problema relacionado .

0
pylang