Python 3: Deep Dive (Part 4 - OOP): オーバーライド (セクション6-2/15)
オブジェクト指向プログラミングにおける単一継承では、オーバーライドを使用して親クラスのメソッドの動作を変更し、子クラスで独自の実装を提供することができます。
メソッドバインディングにより、Pythonは常にインスタンスの実際のクラスから始めてメソッドを探し、その後継承チェーンを上向きに検索していくため、親クラスのメソッドが子クラスのオーバーライドされたメソッドを適切に呼び出すことができます。
拡張を通じて子クラスは親クラスにない全く新しいメソッドや属性を追加できるため、例えば`Person`クラスを継承した`Student`クラスに`study()`メソッドを追加するなど、より特殊化されたクラスを作成することができます。
オブジェクト指向プログラミングにおいて、継承は既存の機能を再利用し修正するだけでなく、特殊化されたサブクラスを作成する際に全く新しい振る舞いを追加することも含みます。オーバーライドと拡張は、子クラスが親から継承したものをカスタマイズし、その上に構築する2つの主要な方法です。ここでは、Pythonのメソッドバインディングと継承チェーンが最終的にどのバージョンのメソッドが呼び出されるかにどのように影響するかなど、オーバーライドメソッドが実際にどのように機能するかを見ていきます。また、サブクラスで追加機能を加える方法も見ていきます。これは一見単純に聞こえるかもしれませんが、属性の検索、`object`メソッドへのデフォルト設定、ベースクラスがどのメソッドを必ず提供すべきか(またはサブクラスに任せるべきか)を決定することを考慮すると、微妙な点が出てくる可能性があります。
オーバーライドによる振る舞いの再定義
サブクラスが継承したメソッドの振る舞いを変更したい場合、オーバーライドによってそのメソッドを再定義することができます。これは`say_bye`のような単純なメソッドから、`repr`や`init`のようなPythonの組み込みの"dunder"メソッドまで、あらゆるものが対象となります。例えば:
class Person:
def say_bye(self):
return "Bye!"
class Student(Person):
def say_hello(self):
return "Yo!"
この例では、`Student`は`Person`から`say_bye`を継承し、`say_hello`を追加しています。サブクラスは機能を置き換えたい場合に`say_bye`をオーバーライドすることもできます:
class Student(Person):
def say_bye(self):
return "Later!"
インスタンスを作成する際、子クラスに存在するオーバーライドされたメソッドは、その子インスタンスから呼び出された場合に「勝利」します。重要なのは、メソッドのインスタンス(`self`)へのバインディングがPythonの解決を駆動するということです。親メソッドが別のメソッドを呼び出す場合(`extended_info`が`info`を呼び出すなど)、子クラスが`info`のオーバーライドを持っている場合、呼び出しメソッドが親に存在していても、子の`info`が使用されます。Pythonは常に現在のインスタンスのクラスから始めて、チェーン内を上向きにメソッドを探します。
組み込みメソッドのオーバーライド
すべてのクラスが最終的に`object`を継承するため、子クラスは`init`、`repr`、`str`などの特殊メソッドをオーバーライドできます。例えば:
class Person:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"Person(name={self.name})"
class Student(Person):
def __repr__(self):
return f"Student(name={self.name})"
ここで、`Student`は`repr`をオーバーライドしながら、`Person`から継承した`init`を使用しています。`repr(some_student)`を呼び出すと、PythonはまずStudentクラスを見て、`repr`があることを確認し、それを使用します。これがオーバーライドの動作です。
親メソッド自体が他のメソッドを呼び出す場合(`str`が`repr`を呼び出す、または親の`extended_info`が`info`を呼び出すなど)、オーバーライドは興味深いものとなります。Pythonは常に子インスタンスを`self`として渡し、呼び出されたメソッドの親バージョンではなく、存在する場合は子バージョンを見つけます。これが、親のコードが子のオーバーライドを呼び出すことがよくある仕組みです。
拡張による特殊化された機能の追加
オーバーライドは既存のものを変更します。一方、拡張は親が全く持っていないメソッドや属性を追加します。Pythonでは、子クラスで新しい属性やメソッドを定義するだけです。一般的な例は:
class Person:
pass
class Student(Person):
def study(self):
return "study... study... study..."
`Student`は今や追加の機能(`study`)を持つ特殊化された`Person`となりました。当然、`Person`のインスタンスは`study()`を呼び出すことはできませんが、`Student`のインスタンスは可能です。`Shape`対`Polygon`対`Square`のようなクラスでも同様のパターンが見られます—各サブクラスはその特殊化された型にのみ適用される新しい特性や振る舞いを追加できます。
ベースクラスがサブクラスによる拡張を想定する場合
時として、ベースクラスは子クラスが特定のメソッドを実装(または拡張)することを期待する場合があります。例えば:
class Person:
def routine(self):
return self.eat() + self.study() + self.sleep()
def eat(self):
return "Person eats..."
def sleep(self):
return "Person sleeps..."
`study()`が`Person`で実装されていない場合、通常の`Person`インスタンスで`routine()`を呼び出すと`AttributeError`が発生します。`study()`を定義する`Student`のようなサブクラスは問題なく動作します:
class Student(Person):
def study(self):
return "Student studies..."
これで`Student().routine()`は`"Person eats...Student studies...Person sleeps..."`で成功します。一方、通常の`Person().routine()`は、フォールバックを提供するか`study`が存在するかを最初にチェックしない限り、失敗する可能性があります。実際の設計では、`study()`を提供せずに直接インスタンス化できないように`Person`を抽象的にすることもできますが、それは基本的な単一継承の範囲を超えています。
クラス対インスタンス属性、およびオーバーライドの適用範囲
Pythonの継承はメソッドとデータ属性の両方をカバーします。サブクラスがクラスレベルの属性を定義する場合(銀行の`Account`例での異なる金利など)、親属性を明示的に参照するか、インスタンス属性でオーバーシャドウしない限り、子インスタンスはその新しい値を見ます。
class Account:
apr = 3.0
def calc_interest(self):
# self.aprを参照
...
class Savings(Account):
apr = 5.0
ここで、`Savings`インスタンスで`calc_interest`を呼び出すと、`self.apr`がインスタンス辞書で見つからない場合は上向きに探すため、サブクラスの`apr = 5.0`を使用します。`calc_interest`内で`Account.apr`と書いた場合、常に3.0を取得し、実質的に`Savings`からオーバーライドされた`apr`を無視することになります。この同じバインディングロジックは、オーバーライドメソッドの動作方法の基礎にもなっています:Pythonは、どの`apr`やメソッドを使用するかを探す際に、親クラスではなく、インスタンスのクラス(この場合は`Savings`)から始めます。
まとめ
オーバーライドは、サブクラスが親の機能を置き換える方法です。`object`から継承したPythonの特殊な"dunder"メソッドを含め、親から継承したあらゆるメソッドをオーバーライドできます。
メソッドバインディングにより、親クラスメソッドが例えば`self.info()`を呼び出す場合、まず子インスタンスのクラスで`info`メソッドを探し、その後チェーンを上に移動します。
拡張は子に全く新しいメソッドや属性を導入します。`Student`は`Person`にない`study()`を追加する可能性があります。
抽象的なパターンは、親クラスが親で実際には定義されていないメソッド(`study()`など)を参照する場合に現れる可能性があります。Pythonでは、メソッドに`NotImplementedError`を発生させるか、正式な抽象基底クラス(ABC)を採用することで対処できます。
クラス対インスタンス属性は同じ継承ルールに従います。`self`または`self.class`を介して参照する代わりに明示的な親クラスへの参照(例:`Account.apr`)を使用する場合、クラス属性のオーバーライドやオーバーシャドウイングは混乱を招く可能性があります。
オーバーライドと拡張自体はかなり直接的な概念ですが、各メソッドまたは属性の呼び出しが特定のインスタンスにバインドされており、Pythonのメソッド解決順序がそのインスタンスのクラスから上に向かって進むことを常に覚えておいてください。これらの原則が組み合わさることで、コードを重複させることなく—そして詳細に慣れてしまえばはるかに混乱が少なく—単一継承で豊かで特殊化された階層を作成することができます。