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


83

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

#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は、渡される動物の性質を知りません。シグナル「speak」を送信し、遅延バインディングを残して、Cat :: talk()またはDog :: think()のいずれのメソッドを呼び出すかを処理します。これは、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では、信号「speak」を任意のオブジェクトに送信できます。オブジェクトがそれを処理できる場合は実行され、そうでない場合は例外が発生します。両方のコードにクラスAirplaneを追加し、AirplaneオブジェクトをmakeSpeakに送信するとします。C ++の場合、AirplaneはAnimalの派生クラスではないため、コンパイルされません。Pythonの場合、実行時に例外が発生しますが、これは予想される動作である可能性もあります。

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

私が指摘したいのは、Pythonで継承を使用する理由はまだ1つも見つからないということです(フレームワークと例外のツリーは別として、別の戦略が存在すると思います)。多態的に実行するために、ベースから派生した階層を実装する必要はありません。継承を使用して実装を再利用する場合は、包含と委任を通じて同じことを実現できますが、実行時に変更できるという追加の利点があり、意図しない副作用のリスクを冒すことなく、包含のインターフェイスを明確に定義できます。

それで、結局のところ、問題は立っています:Pythonの継承のポイントは何ですか?

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

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


18
-1:「委任チェーンでも同じことができます」。本当ですが、継承よりもはるかに苦痛です。クラス定義をまったく使用せずに、複雑な純粋関数をたくさん使用するだけで、同じことを実現できます。同じことを十数通り達成できますが、すべて継承よりも単純ではありません。
S.Lott 2009年

10
確かに私は「私はそれがより良いとは主張していません;)」と言いました
Stefano Borini

4
「Pythonで継承を使用する理由はまだ1つも見つかりません」...確かに「私のソリューションの方が優れている」ように聞こえます。
S.Lott 2009年

9
この印象を与えてくれたらごめんなさい。私の投稿は、Pythonでの継承の使用に関する実際のケースストーリーについて肯定的なフィードバックを得ることが目的でしたが、今日の時点では見つけることができませんでした(主に、すべてのPythonプログラミングで、これが不要なケースに直面したためです。私はそうしました、それは私が上で説明した状況でした)。
ステファノボリーニ

2
実世界の分類法が、オブジェクト指向の例の基礎として役立つことはめったにありません。
アパラーラ2012年

回答:


81

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

私の意見では、継承が正しいアプローチである場合の2つの例を挙げられますが、もっとあると確信しています。

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

次に、明らかにもっと簡単なのは、カプセル化です。これは、オブジェクト指向設計のもう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は4で初期化するのに対し、Snakeは初期化する0で)。

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


3
最初のケースでは、動作ではなく型をチェックしていることを意味します。これは一種の非Pythonです。2番目のケースについては、私は同意します。あなたは基本的に「フレームワーク」アプローチを行っています。あなたはspeak_twiceの実装をリサイクルしています。インターフェースだけでなく、オーバーライドするために、Pythonを検討するときに継承なしで生きることができます。
ステファノボリーニ

9
クラスや関数のような多くのものがなくても生きることができますが、問題はコードを素晴らしいものにするものです。継承はそうだと思います。
Roee Adler

@ StefanoBorini-非常に「ルールベース」のアプローチを取っているようです。しかし、古い決まり文句は真実です。それらは壊れるように作られました。:-)
Jason Baker

@ジェイソンベイカー-私はルールが経験から得られた知恵(例えば間違い)を報告するので好きになる傾向がありますが、創造性がそれらによって妨げられるのは好きではありません。だから私はあなたの声明に同意します。
ステファノボリーニ

1
この例はそれほど明確ではありません。動物、車、形の例は、これらの議論を本当に嫌います:) IMHOが重要なのは、実装を継承するかどうかだけです。もしそうなら、Pythonのルールは本当にjava / C ++に似ています。違いは主にインターフェースの継承です。その場合、ダックタイピングが解決策になることがよくあります-継承よりもはるかにそうです。
David Cournapeau

12

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


11

Pythonでの継承は、何よりも便利です。クラスに「デフォルトの動作」を提供するのに最適であることがわかりました。

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


8

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


3
選択的なオーバーライドが継承の理由です。すべてをオーバーライドする場合、それは奇妙な特殊なケースです。
S.Lott 2009年

1
誰がすべてを上書きしますか?すべてのメソッドがパブリックで仮想であるようにPythonを考えることができます
bashmohandes 2009年

1
@bashmohandes:すべてを上書きすることは決してありません。しかし、質問は、すべてが上書きされる退化したケースを示しています。この奇妙な特殊なケースが質問の基礎です。通常のオブジェクト指向設計では発生しないため、この質問は無意味です。
S.Lott 2009年

7

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

簡単にするために、継承にはインターフェースと実装の2つのタイプがあります。実装を継承する必要がある場合、PythonはC ++のような静的に型付けされたオブジェクト指向言語とそれほど違いはありません。

インターフェイスの継承は大きな違いがあり、私の経験ではソフトウェアの設計に根本的な影響を及ぼします。Pythonのような言語では、その場合に継承を使用する必要はありません。後で間違った設計の選択を修正するのは非常に難しいため、ほとんどの場合、継承を回避することをお勧めします。これは、優れたOOP本で提起されたよく知られたポイントです。

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


6

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

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

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


5

C ++ / Java / etcでは、ポリモーフィズムは継承によって引き起こされます。その誤った信念を放棄し、動的言語があなたに開かれます。

基本的に、Pythonには、「特定のメソッドが呼び出し可能であるという理解」ほどのインターフェースはありません。かなり手が波打っていてアカデミックな響きですね。これは、「speak」を呼び出すため、オブジェクトに「speak」メソッドが必要であることを明確に期待していることを意味します。簡単ですね これは、クラスのユーザーがそのインターフェイスを定義するという点で非常にリスコフ的です。これは、より健康的なTDDに導く優れた設計概念です。

ですから、残っているのは、別のポスターが丁寧に言うのを避けるように管理したように、コード共有のトリックです。各「子」クラスに同じ動作を書き込むことができますが、それは冗長になります。継承階層全体で不変である機能の継承または混合が容易です。一般に、小さいDRY-erコードの方が適しています。


2

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

実際のシステムで継承を使用するたびに、依存関係の絡み合ったウェブにつながるためにやけどを負ったり、継承がなければはるかにうまくいくことに気づきました。今、私はそれを可能な限り避けています。私は単にそれを使用することはありません。

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を別の方法で実行できるとしたら、何を省略しますか?」彼の反応は「クラス」で、それには笑いがありました。しかし、彼は真面目で、本当に問題だったのはクラスではなく、継承だと説明しました。

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

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

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

上記のコードのような関心の分離を適切に行うには、各ステップが実際に付加価値を付けるため、接着方法は必要ありません。したがって、実際には「接着」方法ではありません(付加価値がない場合、設計に欠陥があります)。

それはこれに要約されます:

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

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

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


1

Pythonや他のほとんどすべての言語で継承を回避できます。ただし、コードの再利用とコードの簡素化がすべてです。

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

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

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

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

これを使用すると、哺乳類/爬虫類/鳥のハイブリッド怪物の任意の組み合わせを作成でき、「バーク!」と言うことができます。飛んでフォークした舌を突き出している間、それはそれを適切に処理します!それを楽しんでください!


1

もう1つの小さなポイントは、opの3番目の例では、isinstance()を呼び出すことができないことです。たとえば、3番目の例を別のオブジェクトに渡して、「動物」タイプの呼び出しがそのオブジェクトで話します。そうしないと、犬の種類や猫の種類などを確認する必要があります。バインディングが遅れているため、インスタンスチェックが本当に「Pythonic」であるかどうかはわかりません。しかし、チーズバーガーは話さないので、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"

0

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

私は主に、スーパークラスのメソッドをオーバーライドするために使用される継承を見てきました。たとえば、Pythonのような継承の使用はおそらくもっと多いでしょう。

from world.animals import Dog

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

もちろん、猫は犬の一種ではありませんが、オーバーライドしたいメソッドを除いてDog完全に機能するこの(サードパーティの)クラスがあります。これにより、クラス全体を再実装する手間が省けます。繰り返しますが、whileはのタイプではありませんが、猫は多くの属性を継承します。speakCatDog

メソッドまたは属性をオーバーライドするはるかに優れた(実用的な)例は、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


6
私は…あなたの最初の例に少し混乱しています。最後に確認したところ、猫は一種の犬ではないので、あなたがどのような関係を示しているのかわかりません。:-)
ベンブランク

1
あなたはリスコフの原則をいじっています:猫は犬ではありません。この場合は使用しても問題ないかもしれませんが、Dogクラスが変更されて、たとえば、猫には意味のない「Lead」フィールドが取得された場合はどうなるでしょうか。
Dmitry Risenberg 2009年

1
動物の基本クラスがない場合は、全体を再実装することもできます。これがベストプラクティスだとは言いませんが(動物の基本クラスがある場合は使用してください)、機能し、一般的に使用されます(これは、私が追加した例のように、urllibのユーザーエージェントを変更するための推奨される方法です)
dbr 2009年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.