見出し画像

Python 3: Deep Dive (Part 4 - OOP): ポリモーフィズム (セクション4-1/15)

  • Pythonのポリモーフィズムとダックタイピングは、異なるデータ型に対して同じ演算子や操作を柔軟に適用できる機能で、これにより高い再利用性と表現力のあるコードが実現できます。

  • 特殊メソッド(ダンダーメソッド)を使用することで、カスタムクラスでも文字列表現(`str`、`repr`)や演算子のオーバーロード(`add`など)といったPythonの基本機能を自然に実装できます。

  • これらの機能を組み合わせることで、カスタムオブジェクトをPythonのネイティブな型のように振る舞わせることができ、より直感的で「Pythonic」なコードを書くことができます。

Pythonのオブジェクトモデルには、高度に表現力のあるコードを書くことができる強力な機能が満載されています。これらの機能の中でも、特殊メソッド(ダンダーメソッドとも呼ばれる)と_ポリモーフィズム_が際立っています。特殊メソッドは、イテレーション、コンテキストマネージャー、文字列表現、算術演算子などのPythonの言語構造とシームレスに統合できるようにクラスを強化し、一方でポリモーフィズムは異なるデータ型間で一貫した「ダックタイプ」の動作を保証します。

以下は、これらの概念がPythonでどのように機能するかについての詳細な考察で、文字列表現(`str`と`repr`)の作成と算術演算子の実装に焦点を当てています。すべての例と原則は、Python 3:Deep Dive(パート4 - OOP)のレッスンから得られています。


ポリモーフィズムとダックタイピング

ポリモーフィズムは、異なるデータ型に対して異なる動作をする_一般的な種類の振る舞い_を提供します。Pythonでは、この考え方はダックタイピングと密接に関連しています:

「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルである。」

重要なのは、オブジェクトが必要とする操作をサポートしているかどうかだけです。例えば、オブジェクトが_イテラブルプロトコル_(イテレータを返す`iter`)を実装している場合、それがリスト、タプル、辞書、ジェネレータ、またはカスタムクラスであっても、`for`ループで使用することができます。

演算子におけるポリモーフィズム

ポリモーフィズムは、`+`、`-`、`*`、`/`などの演算子にも拡張されます。`+`が整数を加算したり、文字列を連結したり、リストをマージしたりできることはすでにご存知かもしれません。`add`などのメソッドを実装することで、カスタムオブジェクトをその同じ「プラス演算子」エコシステムに追加することができます。これが演算子オーバーロードにおけるダックタイピングの本質です:「`add`のように鳴くだけでよい」のです。

コメント: Pythonでは、同じ演算子の構文が整数、浮動小数点数、リスト、カスタムオブジェクトで動作するのは、Pythonが舞台裏で適切な特殊メソッド(`add`、`sub`など)を呼び出すためです。


特殊メソッドの概要

Pythonの特殊メソッドは、クラスが言語と優雅に相互作用するために定義できるダブルアンダースコアの「フック」です。例:

  • オブジェクトの作成とコンテキスト

    • `init`(コンストラクタ)

    • `enter` / `exit`(コンテキストマネージャー)

  • コンテナ

    • `getitem`、`setitem`、`delitem`(シーケンスのようなアクセス)

    • `iter`、`next`(イテレーションプロトコル)

    • `len`、`contains`(`len()`と`in`のサポート)

  • 文字列表現

    • `repr`と`str`

  • 算術演算子

    • `add`、`sub`、`mul`、`truediv`、`floordiv`、`mod`、`pow`など

  • 反射演算子とインプレース演算子

    • `radd`、`iadd`など

  • 単項演算子

    • `neg`、`pos`、`abs`

注意: Pythonの特殊メソッド(現在または将来)と衝突を避けるため、独自のメソッドや属性に`something`形式の名前を付けることは避けてください。


`__repr__`と`__str__`による文字列表現

オブジェクトを出力する時、`str(obj)`を呼び出す時、またはオブジェクトを文字列フォーマットに埋め込む時、Pythonは`str`を試みます。それが失敗した場合、`repr`にフォールバックします。REPLまたはJupyterノートブックで変数名を入力するだけの場合、Pythonは直接`repr`を呼び出します。

`__repr__`

  • 開発者向け。

  • 可能な場合、曖昧さを避けるか、オブジェクトを再構築することを目指します。

  • 組み込み関数`repr()`によって呼び出されます。

`__str__`

  • `print0()`と`str()`によって使用されます。

  • 通常はユーザー向けで、より簡潔です。

  • `__str__`が存在しない場合、`__repr__`にデフォルト設定されます。

簡単な例:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __repr__(self):
        print('__repr__ called')
        return f"Person(name='{self.name}', age={self.age})"
    
    def __str__(self):
        print('__str__ called')
        return self.name

p = Person('Python', 30)

print(p)         # __str__が存在する場合はそれを呼び出し、それ以外は__repr__を呼び出す
str(p)           # 上と同じ
repr(p)          # __repr__を呼び出す

算術演算子

基本的な演算子メソッド

  • `add` → +

  • `sub` → -

  • `mul` → *

  • `truediv` → `/`

  • `floordiv` → `//`

  • `mod` → `%`

  • `pow` → `**`

  • `matmul` → `@`(NumPyで行列乗算に使用)

演算子メソッドで`NotImplemented`を返すことは、その操作がサポートされていないことを示します。その場合、Pythonは「反射」操作(例:`radd`)を試みることがあります。

反射演算子

`a + b`を考えてみましょう:Pythonは`a.add(b)`を試みます。それが`NotImplemented`を返し、かつ`a`と`b`の型が異なる場合、Pythonは`b.radd(a)`を呼び出します。`sub`、`mul`などにも同様のペアが存在します。これにより、ベクトルの左オペランドメソッドが整数型を処理しない場合でも、`3 + vector`のような処理が可能になります。

コメント: 反射メソッド(`radd`、`rsub`など)により、左オペランドのメソッドが`NotImplemented`を返した場合に右オペランドが操作を処理できるようになります。

インプレース演算子

  • `iadd` → `+=`

  • `isub` → `-=`

  • `imul` → `*=`

  • ...など

多くの可変クラス(Python `list`など)では、インプレース演算子は左オブジェクトを変更します。ただし、不変オブジェクト(`tuple`、`str`、またはカスタムの不変オブジェクト)は代わりに新しいオブジェクトを返す必要があります。

単項演算子

  • `neg` → 単項マイナス(`-obj`)

  • `pos` → 単項プラス(`+obj`)

  • `abs` → `abs(obj)`


ベクトルクラスの構築(説明例)

以下は、算術メソッドの実装方法を示す簡略化された`Vector`クラスです。コンポーネントを実数として検証し、それらを不変のタプルとして格納し、`add`、`sub`、`mul`、`rmul`を実装します。一部の操作は新しいベクトルを返し(例:スカラー乗算)、他の操作はスカラーを返す(例:内積)場合があることに注意してください。

from numbers import Real

class Vector:
    def __init__(self, *components):
        if len(components) < 1:
            raise ValueError('空のVectorは作成できません。')
        for comp in components:
            if not isinstance(comp, Real):
                raise ValueError(f'Vectorのコンポーネントは実数でなければなりません - {comp}は無効です。')
        self._components = tuple(components)  # 不変として格納

    def __len__(self):
        return len(self._components)

    @property
    def components(self):
        return self._components

    def __repr__(self):
        return f"Vector{self._components}"

    def validate_type_and_dimension(self, other):
        return isinstance(other, Vector) and len(other) == len(self)

    def __add__(self, other):
        if not self.validate_type_and_dimension(other):
            return NotImplemented
        comps = (x + y for x, y in zip(self._components, other._components))
        return Vector(*comps)

    def __sub__(self, other):
        if not self.validate_type_and_dimension(other):
            return NotImplemented
        comps = (x - y for x, y in zip(self._components, other._components))
        return Vector(*comps)

    def __mul__(self, other):
        if isinstance(other, Real):
            # スカラー乗算
            comps = (other * x for x in self._components)
            return Vector(*comps)
        if self.validate_type_and_dimension(other):
            # 内積
            return sum(x * y for x, y in zip(self._components, other._components))
        return NotImplemented

    def __rmul__(self, other):
        # 可換なスカラー乗算のため
        return self * other

    def __neg__(self):
        # 単項マイナス
        comps = (-x for x in self._components)
        return Vector(*comps)

    def __abs__(self):
        # 大きさ(ユークリッドノルム)
        from math import sqrt
        return sqrt(sum(x**2 for x in self._components))

試してみましょう:

v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)       # Vector(4, 6)
print(v1 - v2)       # Vector(-2, -2)
print(v1 * 10)       # Vector(10, 20)
print(10 * v1)       # Vector(10, 20)
print(v1 * v2)       # 内積 => 1*3 + 2*4 = 11
print(-v1)           # Vector(-1, -2)
print(abs(v1))       # sqrt(1^2 + 2^2) = 2.236...

インプレースの例 (`+=`):

def __iadd__(self, other):
    # 同じ次元の場合、selfを変更する可能性がある
    if not self.validate_type_and_dimension(other):
        return NotImplemented
    new_comps = (x + y for x, y in zip(self._components, other._components))
    self._components = tuple(new_comps)
    return self

最終的な考察

特殊メソッドポリモーフィズムを組み合わせることで、Pythonは言語の構文に美しく適合する幅広いカスタム動作を可能にします。オブジェクトの文字列表現の制御(`str`と`repr`)から演算子のオーバーロード(`add`、`sub`など)まで、カスタムオブジェクトをPythonのファーストクラス市民のように感じさせることができます。

  • ダックタイピング: オブジェクトが必要なメソッド/プロトコルを提供することのみを気にします。

  • `str` vs `repr`: オブジェクトのユーザー向けvs開発者向けの文字列表現を提供します。

  • 算術特殊メソッド: `+`、`-`、`*`、`/`などの演算子を、数値的であれ純粋に概念的であれ、意味のある方法でオーバーロードします。

  • 反射演算子とインプレース演算子: `radd`、`iadd`などにより、逆順の式やインプレース式の動作を制御できます。

これらの考え方は、Pythonのクラス設計の基盤を形成し、次のシンプルな原則に集約されます:

オブジェクトを実装するだけでなく、シームレスな「Pythonic」な体験のために、それらをPythonの既存のパターンと構文に統合しましょう。

「超本当にドラゴン」へ

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