web-dev-qa-db-ja.com

namedtupleのサブクラスに追加の初期化を提供するにはどうすればよいですか?

次のようなnamedtupleがあるとします。

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

これにカスタムハッシュ関数を実装したいので、次のサブクラスを作成します。

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

オブジェクトは不変なので、ハッシュ値を1回だけ計算したいので、次のようにします。

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

    def __hash__(self):
        return self._hash

これは機能しているようですが、Pythonでのサブクラス化と初期化について、特にタプルについては、私には本当にわかりません。このソリューションに落とし穴はありますか?これを行うための推奨される方法はありますか?元気?前もって感謝します。

47
Björn Pollex

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__で作成され、ユーザーに返されます。

__new__は、歴史的/奇妙な理由から暗黙的にclsであるため、super__new__staticmethod呼び出しに2回渡す必要があります。

50
habnabit

問題のコードは、多重継承の状況でサブクラス化される場合に備えて、__init__のスーパーコールから恩恵を受ける可能性がありますが、それ以外の場合は正しいです。

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

タプルは読み取り専用ですが、サブクラスのタプル部分のみが読み取り専用ですが、他のプロパティは通常どおりに書き込むことができます。これにより、__init__または__new__で行われるかどうかに関係なく、_hashへの割り当てが可能になります。 __slots__を()に設定することにより、サブクラスを完全に読み取り専用にすることができます。これにより、メモリを節約できるという利点がありますが、_hashに割り当てることはできません。

3
Gordon Wrigley

Python 3.7+)では、 dataclasses を使用してハッシュ可能なクラスを簡単に構築できるようになりました。

コード

intタイプのleftおよびrightを想定して、_unsafe_hash_を介したデフォルトのハッシュを使用します+ キーワード:

_import dataclasses as dc


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


hash(Edge(1, 2))
# 3713081631934410656
_

これで、これらの(可変)ハッシュ可能なオブジェクトをセットの要素または(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)}
_

詳細

あるいは、___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のコメントを拡張すると、OPのカスタムハッシュ関数は信頼できません。特に、属性値は交換できます。 hash(Edge(1, 2)) == hash(Edge(2, 1))、これは意図したものではない可能性があります。

+「unsafe」という名前は、オブジェクトが変更可能であっても、デフォルトのハッシュが使用されることを示しています。これは、特に不変のキーを期待するdictでは望ましくない場合があります。不変ハッシュは、適切なキーワードでオンにすることができます。データクラスの ハッシュロジック および 関連する問題 の詳細も参照してください。

0
pylang