web-dev-qa-db-ja.com

Pythonの継承のポイントは何ですか?

次の状況があるとします

#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インターフェースに非常に似ています。純粋な仮想メソッドのみが含まれているためです。makeSpeakは、渡されたAnimalの性質を知りません。シグナルを送信するだけです。 「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)

さて、この例では同じ戦略が見られます。継承を使用して、犬と猫の両方が動物であるという階層的な概念を活用します。しかし、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では、信号を「話す」ことで任意のオブジェクトに送信できます。オブジェクトがそれを処理できる場合は実行され、そうでない場合は例外が発生します。追加するとします。クラスAirplaneを両方のコードに送信し、AirplaneオブジェクトをmakeSpeakに送信します。C++の場合、飛行機はAnimalの派生クラスではないため、コンパイルされません。Pythonの場合、実行時に例外が発生しますが、これは予想される動作である可能性さえあります。

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

私が指摘したいのは、Python(フレームワークと例外のツリーの一部ですが、代替戦略が存在することを私は推測します)で継承を使用する単一の理由をまだ見つけていないことです)ポリモーフィックに実行するためにベース派生階層を実装する必要はありません。継承を使用して実装を再利用したい場合は、包含と委任によって同じことを実行できます。実行時に変更できるという追加の利点があります。意図しない副作用のリスクを冒すことなく、含まれるインターフェースを明確に定義します。

つまり、結局のところ、問題は山積みです。Pythonの継承のポイントは何ですか?

編集:非常に興味深い回答に感謝します。確かに、コードを再利用するためにそれを使用できますが、実装を再利用するときは常に注意しています。一般に、私は非常に浅い継承ツリーを作成するか、まったくツリーを作成しない傾向があり、機能が一般的である場合は、それを共通モジュールルーチンとしてリファクタリングし、各オブジェクトから呼び出します。変更点が1つあることの利点はわかります(たとえば、犬、猫、ムースなどに追加するのではなく、継承の基本的な利点である動物に追加するだけです)。委任チェーン(JavaScriptなど)。私はそれがより良いと主張しているわけではありません、ただ別の方法です。

この点について 同様の投稿 も見つけました。

81
Stefano Borini

ランタイムのダックタイピングを「オーバーライド」継承と呼んでいますが、継承には設計と実装のアプローチとしての独自のメリットがあり、オブジェクト指向設計の不可欠な部分であると思います。私の控えめな意見では、実際にクラス、関数などなしでPythonをコーディングできるので、何か他のことを達成できるかどうかの問題はあまり関係ありませんが、問題はどれほどうまく設計されているかです。堅牢で読みやすいコードになります。

私の意見では、継承が適切なアプローチである例を2つ挙げることができます。もっとあると思います。

まず、コードを賢くコーディングすると、makeSpeak関数は、その入力が実際に動物であることだけでなく、「話すことができる」だけではないことを検証する必要があります。その場合、最もエレガントな方法は、継承を使用することです。繰り返しになりますが、他の方法で行うこともできますが、それが継承を備えたオブジェクト指向デザインの美点です。コードは、入力が「動物」かどうかを「実際に」チェックします。

2つ目は、カプセル化です。これは、オブジェクト指向設計のもう1つの不可欠な部分です。これは、祖先にデータメンバーまたは非抽象メソッド、あるいはその両方がある場合に関係します。次のばかげた例を考えてみましょう。この場合、祖先にはthen-abstract関数を呼び出す関数(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スタンドアロン関数を実装して、いくつかのアヒル型オブジェクトを受け入れ、speak関数があるかどうかを確認して2回呼び出すことができますが、それはエレガントではなく、ポイントを逃します数値1(それが動物であることを検証します)さらに悪いことに、カプセル化の例を強化するために、子孫クラスのメンバー関数が_"speak_twice"_を使用したいとしたらどうでしょうか。

祖先クラスにデータメンバーがある場合、たとえば_"number_of_legs"_のような祖先の非抽象メソッドで使用される_"print_number_of_legs"_などの子クラスのコンストラクター(たとえば、Dog Snakeは0で初期化するのに対し、4で初期化します。

繰り返しますが、他にも無限に多くの例があると思いますが、基本的には、固体オブジェクト指向設計に基づくすべての(十分に大きい)ソフトウェアには継承が必要です。

79
Roee Adler

Python=の継承はすべてコードの再利用に関するものです。共通の機能を基本クラスに因数分解し、派生クラスにさまざまな機能を実装します。

12

Python=での継承は、何よりも便利です。クラスに「デフォルトの動作」を提供するために使用するのが最適です。

確かに、Python開発者の重要なコミュニティが存在し、継承の使用にまったく反対しています。何をしても、無理をしないでください。過度に複雑なクラス階層を持つことは確実です「Javaプログラマー」というラベルを付ける方法、そしてあなたはそれを持つことができないだけです:-)

10
Jason Baker

Pythonの継承のポイントはコードをコンパイルすることではなく、クラスを別の子クラスに拡張する継承の本当の理由であり、基本クラスです。ただし、Pythonで入力すると、「インターフェース」の概念が役に立たなくなります。クラス構造を制限するためにインターフェースを使用する必要がなく、呼び出し前にメソッドが存在するかどうかを確認できるだけだからです。

8
bashmohandes

このような抽象的な例で意味のある具体的な答えを出すことは非常に難しいと思います...

簡単にするために、継承にはインターフェースと実装の2つのタイプがあります。実装を継承する必要がある場合、pythonは静的に入力されたOO C++のような言語とそれほど変わりません。

インターフェースの継承は大きな違いがあり、私の経験ではソフトウェアの設計に根本的な影響があります。 Pythonのような言語は、その場合に継承を使用することを強制しません。継承を回避することは、多くの場合、適切な設計選択を後で修正することが非常に難しいため、ほとんどの場合良い点です。それはすべての良いOOP本で提起された有名なポイント。

プラグインなど、Pythonでインターフェイスの継承を使用することをお勧めする場合があります。そのような場合、Python 2.5以下では、「組み込み」のエレガントなアプローチが欠けています。 、およびいくつかの大きなフレームワークが独自のソリューションを設計しました(zope、trac、twister)Python 2.6以降には これを解決するためのABCクラス があります。

7

ダックタイピングが無意味になるのは継承ではなく、インターフェイスです。つまり、すべて抽象的な動物クラスを作成する際に選択したインターフェイスと同じです。

子孫が実際の振る舞いを導入するために動物クラスを使用していた場合、追加の振る舞いを導入した犬と猫のクラスには両方のクラスの理由があります。引数が正しいのは、祖先クラスが子孫クラスに実際のコードを提供していない場合のみです。

Pythonは任意のオブジェクトの機能を直接知ることができ、これらの機能はクラス定義を超えて変更可能であるため、純粋な抽象インターフェースを使用して、どのメソッドを呼び出すことができるかをプログラムに「伝える」という考え少し無意味ですが、それが唯一の、あるいは主要な継承ポイントではありません。

6
Larry Lustig

C++/Java/etcでは、ポリモーフィズムは継承によって引き起こされます。忘れられていた信念を放棄し、ダイナミックな言語があなたを開きます。

本質的に、Pythonには、「特定のメソッドが呼び出し可能であるという理解」ほどのインターフェースはありません。かなり手で波打っていて、学術的に聞こえますか?それは、「speak」を呼び出すからです。オブジェクトに「speak」メソッドが必要であることが明確にわかりますか?単純でしょ?これは、クラスのユーザーがインターフェイスを定義するという点で非常にリスコフ的であり、より健全なTDDに導く優れた設計概念です。

だから、残っているのは、別のポスターが言ったことを丁寧に避けたので、コード共有のトリックです。同じ動作を各「子」クラスに書き込むことができますが、それは冗長です。継承階層全体で不変である機能を継承またはミックスインするのが簡単です。一般的には、小さい、DRY-erコードの方が優れています。

5
agileotter

Pythonや他のほとんどの言語)で継承を回避できますが、それはすべてコードの再利用とコードの単純化に関するものです。

セマンティックトリックにすぎませんが、クラスと基本クラスを構築した後は、オブジェクトで何ができるかを知って、それが可能かどうかを確認する必要さえありません。

動物をサブクラス化した犬であるdがあるとします。

command = raw_input("What do you want the dog to do?")
if command in dir(d): getattr(d,command)()

ユーザーが入力したものが利用できる場合、コードは適切なメソッドを実行します。

これを使用して、哺乳類/爬虫類/鳥のハイブリッド怪物を好きなように組み合わせて作成できます。これで、「吠えろ!」と言うことができます。飛んでその二股の舌を突き出しながら、それはそれを適切に処理します!それを楽しんでください!

1
mandroid

もう1つの小さな点は、操作の3番目の例です。isinstance()を呼び出すことはできません。たとえば、3番目の例を別のオブジェクトに渡して、「Animal」タイプを呼び出して、それについて話します。そうしないと、犬の種類、猫の種類などを確認する必要があります。バインディングが遅いため、インスタンスチェックが本当に「Python」であるかどうかはわかりません。しかし、チーズバーガーが話さないため、AnimalControlがチーズバーガータイプをトラックに投げ込もうとしない方法を実装する必要があります。

class AnimalControl(object):
    def __init__(self):
        self._animalsInTruck=[]

    def catachAnimal(self,animal):
        if isinstance(animal,Animal):
            animal.speak()  #It's upset so it speak's/maybe it should be makesNoise
            if not self._animalsInTruck.count <=10:
                self._animalsInTruck.append(animal) #It's then put in the truck.
            else:
                #make note of location, catch you later...
        else:
            return animal #It's not an Animal() type / maybe return False/0/"message"
1
yedevtxt

継承にはあまり意味がありません。

継承を実際のシステムで使用したことがあるたびに、依存関係の絡み合いが発生したためにやけどしたり、時間をかけずに継承することではるかに優れていることに気づきました。今、私はそれをできるだけ避けます。私は単にそれを使用することはありません。

class Repeat:
    "Send a message more than once"
    def __init__(repeat, times, do):
        repeat.times = times
        repeat.do = do

    def __call__(repeat):
        for i in xrange(repeat.times):
             repeat.do()

class Speak:
    def __init__(speak, animal):
        """
        Check that the animal can speak.

        If not we can do something about it (e.g. ignore it).
        """
        speak.__call__ = animal.speak

    def twice(speak):
        Repeat(2, speak)()

class Dog:
     def speak(dog):
         print "Woof"

class Cat:
     def speak(cat):
         print "Meow"

>>> felix = Cat()
>>> Speak(felix)()
Meow

>>> fido = Dog()
>>> speak = Speak(fido)
>>> speak()
Woof

>>> speak.twice()
Woof

>>> speak_twice = Repeat(2, Speak(felix))
>>> speak_twice()
Meow
Meow

ジェームズゴスリングは記者会見で一度、次のような質問を受けました。「前に戻ってJava違えば、何を省きますか?」。彼の回答は「クラス」でした。笑いがありましたが、彼は真面目で、本当に問題だったのはクラスではなく継承だと説明しました。

私はそれを薬物依存症のように見ている-それはあなたに気分が良い迅速な修正を与えるが、結局それはあなたを混乱させる。つまり、これはコードを再利用する便利な方法ですが、子クラスと親クラスの間の不健全な結合を強制します。親を変更すると、子が壊れる可能性があります。子は特定の機能について親に依存しており、その機能を変更することはできません。したがって、子によって提供される機能は親にも関連付けられます-両方を持つことができます。

構築時に構成される他のオブジェクトの機能を使用して、インターフェースを実装するインターフェースに単一のクライアント向けクラスを1つ提供することをお勧めします。適切に設計されたインターフェースを介してこれを行うと、すべての結合が排除され、高度に構成可能なAPIが提供されます(これは新しいことではありません。ほとんどのプログラマーはすでにこれを行っていますが、十分ではありません)。実装クラスは単に機能を公開する必要があるわけではないことに注意してください。そうでない場合、クライアントは構成されたクラスを直接使用する必要があります-それはその機能を組み合わせることによって新しい何かをしなければなりません

純粋な委任の実装は、委任の「チェーン」を介して値を渡すだけの多くの「接着剤」メソッドを必要とするため、継承キャンプからの議論があります。ただし、これは、委譲を使用して、継承に似たデザインを再発明するだけです。継承ベースの設計に長年さらされてきたプログラマーは、このトラップに陥る可能性が特に高くなります。これは、気づかないうちに、継承を使用して何かを実装し、それを委任に変換する方法を考えるためです。

上記のコードのような懸念事項の適切な分離には、各ステップが実際に値を追加するため、実際には「接着剤」メソッドではないため、接着剤メソッドは必要ありませんまったく(付加価値がない場合、デザインに欠陥があります)。

要約すると次のとおりです。

  • 再利用可能なコードの場合、各クラスは1つのことだけを実行する必要があります(適切に実行する必要があります)。

  • 継承は、親クラスと混同されるため、複数のことを行うクラスを作成します。

  • したがって、継承を使用すると、クラスの再利用が困難になります。

1
Mike A

Pythonのクラスは、基本的には一連の関数とデータをグループ化する方法にすぎません。C++などのクラスとは異なります。

スーパークラスのメソッドのオーバーライドに使用される継承を見てきました。たとえば、おそらくPythonのように継承を使用するとします。

from world.animals import Dog

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

もちろん猫は犬のタイプではありませんが、私はこれを持っています(サードパーティ)Dogクラスは完全に機能しますexceptオーバーライドしたいspeakメソッド-これにより、クラス全体の再実装が省かれ、ニャーと鳴きます。繰り返しますが、CatDogのタイプではありませんが、猫は多くの属性を継承します。

メソッドまたは属性をオーバーライドするはるかに優れた(実用的な)例は、urllibのユーザーエージェントを変更する方法です。あなたは基本的にサブクラスurllib.FancyURLopenerおよびバージョン属性を変更します( ドキュメントから ):

import urllib

class AppURLopener(urllib.FancyURLopener):
    version = "App/1.7"

urllib._urlopener = AppURLopener()

例外が使用される別の方法は、継承がより「適切な」方法で使用される場合の例外です。

class AnimalError(Exception):
    pass

class AnimalBrokenLegError(AnimalError):
    pass

class AnimalSickError(AnimalError):
    pass

..次に、AnimalErrorをキャッチして、それから継承するすべての例外、またはAnimalBrokenLegErrorのような特定の例外をキャッチできます。

0
dbr