1. ホーム
  2. python

[解決済み] Pythonで継承する意味とは?

2023-07-11 15:26:56

質問

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

#include <iostream>

class Animal {
public:
    virtual void speak() = 0;
};

class Dog : public Animal {
    void speak() { std::cout << "woff!" <<std::endl; }
};

class Cat : public Animal {
    void speak() { std::cout << "meow!" <<std::endl; }
};

void makeSpeak(Animal &a) {
    a.speak();
}

int main() {
    Dog d;
    Cat c;
    makeSpeak(d);
    makeSpeak(c);
}

見ての通り、makeSpeakは一般的なAnimalオブジェクトを受け取るルーチンです。この場合、Animalは純粋な仮想メソッドのみを含むので、Javaのインターフェイスに非常に似ています。シグナル "speak "を送るだけで、Cat::speak() か Dog::speak() のどちらのメソッドを呼び出すかは、レイトバインディングに任されます。つまり、makeSpeakに関する限り、どのサブクラスが実際に渡されたかという知識は重要ではありません。

しかし、Pythonではどうでしょうか?同じケースをPythonで書いたコードを見てみましょう。ちょっとC++のケースとなるべく似ていることに注意してください。

class Animal(object):
    def speak(self):
        raise NotImplementedError()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

さて、この例でも同じような戦略が見られます。DogsとCatsの両方がAnimalsであるという階層的な概念を利用するために継承を使用しています。 しかし、Pythonでは、このような階層は必要ありません。これは同じように機能します

class Dog:
    def speak(self):
        print "woff!"

class Cat:
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

Pythonでは、好きなオブジェクトにシグナル "speak "を送ることができます。そのオブジェクトが対応可能であれば実行され、そうでなければ例外が発生します。両方のコードにAirplaneクラスを追加し、AirplaneオブジェクトをmakeSpeakに提出したとします。C++の場合、AirplaneはAnimalの派生クラスではないので、コンパイルされません。Pythonの場合、実行時に例外が発生しますが、これは期待された動作である可能性さえあります。

一方、speak()メソッドを持つMouthOfTruthクラスを追加したとします。C++ の場合、階層をリファクタリングするか、MouthOfTruth オブジェクトを受け入れるために別の makeSpeak メソッドを定義するか、Java では CanSpeakIface に動作を抽出して、それぞれのインターフェイスを実装する必要があるでしょう。いろいろな解決策がありますが...。

私が指摘したいのは、Pythonで継承を使う理由はまだ一つも見つかっていないということです(フレームワークや例外の木は別として、代替戦略は存在すると思います)。もし実装を再利用するために継承を使いたいのであれば、封じ込めと委譲によって同じことを達成できますし、実行時にそれを変更することができるという利点があり、意図しない副作用のリスクなしに、含まれるもののインターフェースを明確に定義することができます。

結局のところ、Pythonで継承をする意味はあるのか、という疑問が残ります。

編集 : 大変興味深いご回答をありがとうございました。確かにコードの再利用には使えますが、実装を再利用する場合は常に注意が必要です。一般的に、私は非常に浅い継承ツリーか、全くツリーを作らない傾向があり、もし機能が共通であれば、それを共通のモジュールルーチンとしてリファクタリングし、各オブジェクトからそれを呼び出すようにしています。しかし、同じことはデリゲーションチェーン(JavaScriptのようなもの)でも実現できます。私はそれがより良いとは主張しませんが、単に別の方法です。

また、私は 同様の投稿 を見つけました。

どのように解決するのですか?

あなたは実行時のダックタイピングを継承のオーバーライドと呼んでいますが、私は継承はオブジェクト指向の設計と実装のアプローチとして独自の利点があると信じています。なぜなら、クラスや関数などがなくてもPythonをコーディングすることはできますが、問題は、あなたのコードがどれだけうまく設計され、堅牢で読みやすいものになるかです。

私の意見では、継承が正しいアプローチである例を2つ挙げることができます。

まず、賢くコーディングすれば、makeSpeak関数は入力が本当に動物であることを検証したいかもしれませんし、"それが話すことができるということだけでなく、その場合、最もエレガントな方法は継承を使用することでしょう。この場合、最もエレガントな方法は、継承を使うことです。

2つ目は、明らかにもっと簡単なことですが、オブジェクト指向設計のもうひとつの重要な部分であるカプセル化です。これは、祖先がデータ メンバーや抽象化されていないメソッドを持つ場合に関連してきます。次の愚かな例を見てみましょう。先祖が関数 (speak_twice) を持ち、その関数が抽象関数を呼び出しています。

class Animal(object):
    def speak(self):
        raise NotImplementedError()

    def speak_twice(self):
        self.speak()
        self.speak()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

仮定の話 "speak_twice" が重要な機能であるとして、それをDogとCatの両方でコーディングしたくはないでしょうし、この例から推測することができると思います。もちろん、Pythonのスタンドアロン関数を実装して、duck-typedオブジェクトを受け取り、それがspeak関数を持っているかどうかをチェックし、それを2回呼び出すことはできますが、それはエレガントではなく、ポイント1(それが動物であることの検証)を見逃しています。さらに悪いことに、カプセル化の例を強化するために、子孫クラスのメンバ関数が "speak_twice" ?

祖先のクラスがデータ・メンバを持つ場合、さらに明確になります。 "number_of_legs" のような祖先の非抽象メソッドで使用されるデータメンバがある場合、それは明確になります。 "print_number_of_legs" のような抽象的でないメソッドで使用されますが、子孫クラスのコンストラクタで初期化されます (例えば、Dog は 4 で初期化するのに対し、Snake は 0 で初期化します)。

繰り返しますが、もっとたくさんの例があると思いますが、基本的に、しっかりとしたオブジェクト指向の設計に基づくすべての(十分に大きな)ソフトウェアは、継承を必要とします。