見出し画像

Python 3: Deep Dive (Part 4 - OOP): 特殊メソッド (セクション4-2/15)

  • Pythonの特殊メソッド(`lt`、`eq`、`hash`など)を実装することで、カスタムクラスに比較機能や等価性チェックを追加できる。

  • オブジェクトのハッシュ化と真偽値の判定は、`hash`と`bool`(または`len`)メソッドを通じて制御でき、これによりオブジェクトを辞書のキーやif文で使用可能になる。

  • `call`メソッドを実装することで、通常のクラスインスタンスを関数のように呼び出し可能にでき、デコレータやステート付き関数などの高度な機能を実現できる。

Pythonでは、私たちはよくダックタイピングとポリモーフィズムについて話します—これらは、私たちのクラスを言語とシームレスに統合させる概念です。最初の投稿では、文字列表現、算術演算子、およびその他の特殊メソッドの作成について探りました。今回は、「リッチ比較」、ハッシュ化、ブール値、およびオブジェクトを呼び出し可能にすることに焦点を当てます。

以下では、単純な特殊メソッド(__lt__、__eq__、__hash__、__bool__、__call__など)を実装することで、カスタムクラスでPythonらしい強力な動作を実現する方法を見ていきます。


リッチ比較

リッチ比較メソッドの概要

Pythonのリッチ比較特殊メソッド:

  • `lt` (より小さい)

  • `le` (以下)

  • `eq` (等しい)

  • `ne` (等しくない)

  • `gt` (より大きい)

  • `ge` (以上)

算術演算子とは異なり、Pythonの比較リフレクションは、両方のオペランドが同じ型であっても機能します。たとえば、`a < b`が`NotImplemented`を返す場合、Pythonは自動的に`b > a`を試みます。同様に、`a == b`が実装されていない場合、Pythonは`b == a`を試みます。これにより、逆演算と異なる型の比較でも成功する可能性があります。

コメント:`ne`(等しくない)を明示的に実装しない場合、Pythonは内部で`eq`を使用して、`a != b`を`not(a == b)`として生成します。同様に、`gt`を定義しない場合、Pythonはオペランドを逆にして`lt`を試みます。

__eq__と__lt__の実装

2次元ベクトルの簡単な例でアプローチを示します:

from math import sqrt

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector(x={self.x}, y={self.y})"

    def __eq__(self, other):
        # (x, y)タプルまたは別のVectorと比較
        if isinstance(other, tuple):
            other = Vector(*other)
        if isinstance(other, Vector):
            return self.x == other.x and self.y == other.y
        return NotImplemented

    def __abs__(self):
        return sqrt(self.x ** 2 + self.y ** 2)

    def __lt__(self, other):
        # 大きさを比較
        if isinstance(other, tuple):
            other = Vector(*other)
        if isinstance(other, Vector):
            return abs(self) < abs(other)
        return NotImplemented

リフレクションの例

  • `v1 < v2`:`v1.lt(v2)`を呼び出します。これが`NotImplemented`を返す場合、Pythonは`v2.gt(v1)`を試みます。

  • `(1,1) > v1`:タプル`(1,1)`は`Vector`との比較方法を知らないため、`NotImplemented`を返します。Pythonはオペランドを反転させ、代わりに`v1 < (1,1)`を呼び出します。

le、ge、neなどの処理

  • `ne`はしばしば`eq`にフォールバックします:`ne`を実装していない場合、Pythonは`a != b`に対して`not (a == b)`を使用します。

  • `le`は`lt`と`eq`を組み合わせて`self == other or self < other`として実装できます。

  • 同様に、`ge`は逆のチェックを行うかもしれません:`other <= self`。

`functools.total_ordering`

`eq`と`lt`などの順序付けメソッドの少なくとも1つを提供すれば、1つのデコレータで不足している比較を補完できます。例えば:

from functools import total_ordering

@total_ordering
class Number:
    def __init__(self, x):
        self.x = x

    def __eq__(self, other):
        if isinstance(other, Number):
            return self.x == other.x
        return NotImplemented

    def __lt__(self, other):
        if isinstance(other, Number):
            return self.x < other.x
        return NotImplemented

これで`a <= b`、`a > b`、`a >= b`などが自動的に機能します。


ハッシュ化と等価性

ハッシュ化が重要な理由

オブジェクトを辞書やセットで使用するためには、以下が必要です:

  1. 有効な`hash`メソッド。

  2. 有効な等価性の定義(`eq`)。

重要なルール:2つのオブジェクトが等しいと見なされる場合、それらは同じハッシュ値を持たなければなりません。 Pythonは`eq`をオーバーライドする場合、デフォルトで`hash = None`を設定し、セット/辞書での矛盾した使用を防ぎます。

コメント:`eq`を定義する場合、`hash`も定義しない限り、Pythonはハッシュ化を無効にします。これにより、「等しい」と比較されるが異なるハッシュ値を持つオブジェクトを持つことができなくなります。

class Person:
    def __init__(self, name):
        self._name = name   # 名前を不変に保つ

    @property
    def name(self):
        return self._name

    def __eq__(self, other):
        return isinstance(other, Person) and self.name == other.name

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

ここでは:

  • `name`を「読み取り専用」プロパティ(`_name`)に格納して不変に保ちます。

  • `name`を比較して等価性(`eq`)を定義します。

  • `self.name`をハッシュ化して`hash`を定義します。

このように、2つの`Person("Eric")`オブジェクトは等しく、同じハッシュ値を共有するため、辞書のキーとして使用できます:

p1 = Person("Eric")
p2 = Person("Eric")

d = {p1: "最初のEric"}
print(d[p2])  # 最初のEric、p2は p1と同じハッシュ値を持ち、==で等しいため

ブール真偽値

Pythonでは、任意のカスタムオブジェクトには関連する真偽値があります:真値か偽値のいずれかです。デフォルトでは、特定のメソッドを定義しない限り、カスタムオブジェクトは常に真値となります:

  1. `bool(self) -> bool`

    • このメソッドが存在する場合、Pythonはこれを直接使用して`bool(obj)`を決定します。

    • 実際のPythonの`True`または`False`を返す必要があり、整数や他の型ではいけません。

  2. `bool`が欠けている場合、Pythonは`len() -> int`をチェックします。

    • 長さが0の場合は`False`、それ以外は`True`を返します。

  3. どちらも定義されていない場合、オブジェクトはデフォルトで`True`となります。

boolの例

class Point2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __bool__(self):
        # 原点は偽値、それ以外は真値
        return not (self.x == 0 and self.y == 0)

p1 = Point2D(0, 0)
p2 = Point2D(1, 1)

print(bool(p1))  # False
print(bool(p2))  # True

lenの例

class MyList:
    def __init__(self, length):
        self._length = length

    def __len__(self):
        return self._length

l1 = MyList(0)
l2 = MyList(10)
print(bool(l1))  # False
print(bool(l2))  # True

オブジェクトを呼び出し可能にする

通常の関数やメソッドの呼び出し可能性を超えて、`call`を定義することで任意のオブジェクトを呼び出し可能にできます。典型的な使用例は、独自の内部状態を保持する関数のようなオブジェクトを作成することや、クラスを介してデコレータを実装することです。

単純な例

class Adder:
    def __init__(self, increment):
        self.increment = increment

    def __call__(self, value):
        return value + self.increment

add5 = Adder(5)
print(add5(10))  # 15

コメント:Pythonで「関数」に見えるものの多くは、実際には`call`メソッドを持つインスタンスです(例:`functools.partial`)。

デコレータの例

`call`を使用してデコレータクラスを実装できます。これは、装飾された関数の呼び出しに関する情報を追跡できます。例えば、呼び出し回数と平均実行時間を測定するプロファイラ:

from time import perf_counter

class Profiler:
    def __init__(self, fn):
        self.fn = fn
        self.counter = 0
        self.total_elapsed = 0

    def __call__(self, *args, **kwargs):
        self.counter += 1
        start = perf_counter()
        result = self.fn(*args, **kwargs)
        end = perf_counter()
        self.total_elapsed += (end - start)
        return result

    @property
    def avg_time(self):
        return self.total_elapsed / self.counter

@Profiler
def slow_add(a, b):
    from time import sleep
    import random
    sleep(random.random())
    return a + b

print(slow_add(1, 2))
print(slow_add(10, 20))
print(slow_add.counter)   # 呼び出された回数
print(slow_add.avg_time)  # 平均実行時間

内部的には、`slow_add`は元の関数をラップするカスタム`call`メソッドを持つ`Profiler`インスタンスになっています。


主なポイント

  1. リッチ比較:`eq`、`lt`などを実装することで、クラスに直感的な比較動作を与えることができ、必要に応じてリフレクションにフォールバックできます。

  2. ハッシュ化と等価性:`eq`をオーバーライドする場合は、`hash`もオーバーライドする必要があります。辞書やセットの矛盾を避けるため、ハッシュ化に使用するデータは不変であるべきです。

  3. ブール値:`bool`または`len`を定義しない限り、クラスは常に真値です。`bool`が存在する場合、Pythonはこれを直接呼び出します—それ以外の場合、Pythonは`len`が`0`を返すかどうかをチェックします。

  4. 呼び出し可能オブジェクト:インスタンスに`call`を定義することで、関数のように使用できるものに変換します。これは関数のようなオブジェクト(部分適用、デコレータ、キャッシュロジックなど)を作成する際によく使用されます。

これらの特殊メソッドを慎重に設計することで、より簡潔で、エレガントで、Pythonらしいコードを書くことができます。等価性チェックのカスタマイズ、デコレータクラスの構築、または`if`文でのオブジェクトの動作制御など、特殊メソッドはPythonですべてを自然に感じさせる魔法です。


「超本当にドラゴン」へ

いいなと思ったら応援しよう!