作者: 機械伯爵
日時: 2002/4/3(14:10)
P-5 この子どこの子?
  〜多重継承是非論〜

 さて、オブジェクト指向主義の鬼っ子、多重継承の話です。

 多重継承については、プログラムの一般常識からするとやや特殊な分野の話題に
なりますので(言語について、見識の深い諸氏には釈迦に説法ながら)、その内容
と、それにまつわる諸問題について、あらかじめ簡単に記しておきたいと思います。

 オブジェクト指向プログラミング言語では、一般的な見方としてはクラスの継承
が必須条件となります(ですから、JavaScriptなどは、基本的にはオブジェクトラ
イクであって、正式なオブジェクト指向言語*0とは普通言いません)

 多重継承とは、継承において、複数のスーパークラスの特徴(インターフェイス、
あるいはメンバ)を持ったサブクラスをつくることを意味します。

 具体的には、クラスAとクラスBの両方から、直接継承するクラスCを作れる、とい
うことです(継承するクラスは、2つに限らず、それ以上でもかまいません。ただ
しこの場合、クラスAを継承してクラスBをつくり、それを継承してクラスCを作る、
ということは意味しません)

 2つ以上のクラスの機能を合成した新しいクラスが手軽に出来ますので、非常に
便利な反面、ちゃんとクラスの動作を理解していないと、思わぬバグに発展するこ
とが予想されます。

 たとえば、複数の親クラスに同じ名前のメソッドが存在した場合、メソッドのオー
バーライド(遮蔽)*1が発生します。

 あるスーパークラスのサブクラスとして当然の動きを期待していたメソッドが、
実は継承の時点で他のスーパークラスによって上書きされ、ぜんぜん違った動きを
してしまう、ということもありえるわけです。

 ベンダーが提供するクラスライブラリを使った場合など、かなり悲惨です。

 自分が1からつくったものなら検証も可能ですが、ベンダーの提供されるものに
はソースが附属していない場合も多く、いったいどんな動きをするものやら検討が
つかない、というものも多くあります。

 たとえオープンソースであったとしても、多重継承を多用したクラスの機能は、
全貌を解明するのが難しく(親クラスの存在が指数関数的に増える可能性がある)、
手間隙が必要以上にかかってしまう、という困った事態におちいるわけです。

 そのため、クラス機能の合成については、多重継承のデメリット面があまりにも
大きいとみなされ、現在では2つ以上のクラスインスタンスをフィールドとして持
つラッパクラスを作ってそれを利用することが、デザインパターンなどで推奨され
ています。

 ところが多重継承は、「型のある言語」にとっては*2、単に2つのクラスの機能を
合成するという機能以上の意味を持ちます。

 C++やJavaのような「型のある言語」では、関数やメソッドを定義する際、引数と
してうけとる値の型をあらかじめ指定しておく必要があります。

 よって、それ以外の型を引数として指定すると、エラーを生じるわけです。

 これは「型チェック機能」と呼ばれ、型のある言語において、不正なコーディン
グを防止する強力な手段となるわけですが、この機能と真っ向から矛盾するのが、
オブジェクト指向言語における「多態性(ポリモルフィズム)」です。

 多態性にはさまざまな意味がありますが、この場合は、異なるクラスのインスタ
ンスであっても同じように引数として受け取り、それぞれの引数の型によって(そ
れぞれの引数が内部で持っている処理用ルールに従って)処理できる、というもの
です。

 多態性の裏にはオブジェクト指向主義の目的の一つである内部隠蔽と、委譲(オ
ブジェクトの振る舞いは、オブジェクト自身によって定義される)という考え方が
絡んできますので、当然無視できません。

 幸い、型のある言語であっても、継承したスーパークラスとして扱える(あるいは
キャストによって変換できる)というルールがありますので、それを用いるわけです
が、単一継承でそれを再現しようとすると、クラス設計における考え方*3に反する、
わかりづらい継承になることが避けられません。

 そもそも、オブジェクト指向主義とは、プログラミングの際に人間が理解しやす
いような仕掛けなので、それがわかりづらくなるとすれば本末転倒です。

 そこで、機能部分を書いたクラスを合成することによって、すっきりとした形で
多態性と型チェック機能を両立させようという工夫が多重継承、ということになり
ます。

 ですから多重継承は、多態性を実現させようとして注意深く使えば有効なのです
が、いかんせん人間のあさましさ、機能合成という禁断の果実に手を出してしまう
人が後をたたず、多重継承自体の有用性より害毒が強調される結果となりました
(なぜなら、多態性は基本的に、自分以外の利用者のための配慮であり、合成は往々
にして、自分の手抜きのため、ですから)

 Javaでは、多重継承の濫用をふせぐためにインターフェイスという擬似クラスを
設定し、インターフェイス以外の通常のクラスが多重継承できないように、また、
振る舞いが単一のクラスの定義を調べることによって分かるような工夫がなされて
います(なお、C++でも抽象クラスを使えば同様の結果が得られるのですが、根本的
な禁止策はほどこされていないのが事実です)

 さて、上のような事情がある以上「型のある言語」はともかく、「型の無い言語」
には無用の長物、ということで、SmalltalkでもRubyでも、多重継承はできないよう
に最初から設計されています。

 そもそも、引数の型など指定しないのですから、必要な振る舞いさえ定義されて
いれば、「型の無い言語」にとっては、何の問題もないわけです。

 ・・・・・で、Pythonです・・・・・。

 自分で話しといてなんですが、この話題については、非常に分が悪いです。

 なにせ、いくら頭をひねっても、「型の無い言語であるPythonが、多重継承をサ
ポートしなければならない理由」など、全くといっていいほど無いからです。

 唯一「非常にか細い声で」主張するなら「だって所詮スクリプト言語だし・・・」
でしょうか。

 クラス合成が問題になってくるのは、クラスライブラリが巨大だったり、ソース
が隠蔽されていたりする場合です。

 Pythonが提供しているクラスライブラリは、ほとんど多重継承してないようです
し、自分でちょこちょこっと書いて自分で使う分には、手間のかからないほうが良
い、ということもあります。

 無論それは、今まで「Python的」と言ってきていた特長とはあまりにも矛盾して
いますが、多重継承も、やりかたによっては(ごくごくまれに)コードがすっきり
して見やすくなることもあります。

 しかし当然、濫用は禁物です。

 特に、いくらシンプルなスクリプトでも、親クラス同士のメソッドの遮蔽が起こ
るような事態は、意識して避けるべき*4でしょう。

 自由度の高い言語は、その自由の責任は自分でとらなければならない、という代
償を払う必要があります。

 Pythonに関しても、それは同じです。

 結論・・・Pythonに於いても、多重継承は使うべからず(Rubyのグローバル変数
と同じ)



*0正式なオブジェクト指向言語

 「完全なオブジェクト指向言語」では、さらに条件がきびしくなり、私の知る限
りではSmalltalk以外は知りません。
 ならオブジェクト指向などと言わず、Smalltalkライクといえばいいのに、そのご
先祖のSimulaが話しをややこしくしてるので・・・


*1メソッドのオーバーライド(遮蔽)

 オーバーロード(多重定義)と混乱するので、オーバーライドという言葉は日本
ではあまり使われず、もっぱら遮蔽と呼ばれていますが、オーバーロードを再定義
と訳したりして(私も混乱しました)意味自体も混乱しているのが現状です。
 ちなみにオーバーロードは多重定義のことで、メソッドのオーバーロードといえ
ば、種類(型)の違う引数に対して、メソッドが別の振る舞いをすることを指しま
す。
 それに対してオーバーライドといえば遮蔽、すなわち本来は親クラスが持ってい
たメソッドの定義を子クラスで再定義することを意味します。
 この場合、親クラスと子クラスでは、メソッドの意味(振る舞い)が違う、とい
うことになります。
 よって、型の無いPythonやRubyでは、演算子についてはオーバーロードできるわ
けがなく、本来はオーバーライド(遮蔽/再定義)だと思うのだけど、演算子のオー
バーライドとは普通言わないらしく、オーバーロードで統一されてます。
 型の無い言語では、オーバーロードはメソッドの中で振り分けて処理するのが普
通です。


*2型のある言語にとっては・・・

 型の無い言語では、必要なメソッド(インターフェイス)さえ備えていれば、型
を考える必要がありませんので、継承によって生まれたサブクラスに対して、サブ
タイプと呼びます。


*3クラス設計における考え方

 オブジェクト指向言語で「継承」を行うのは、「is-aの関係」即ち「〜は〜であ
る」の関係にあるときのみ、という鉄則があります。
 つまり、「飛ぶもの」を処理するための手続きがあったとして、これで「こうも
り」を定義しようとする場合、型の無い言語なら「飛ぶ」という項目があるだけで
処理可能となります。
 ところが型のある言語の場合、「ほ乳類」から派生した「こうもり」と、別個の
「鳥類」から派生した「はと」を同じ手続きで処理しようとすると、「ほ乳類」と
「鳥類」を派生させた「は虫類」全体を「飛ぶもの」のためのクラスとして認識さ
せるか、あるいは「ほ乳類」から「鳥類」を派生させるという処理が必要となるわ
けです。
 どちらも不自然ですので、この場合は「鳥類」と「こうもり」の両方に「飛ぶも
の」という型を親として与えるのが一番自然になります。
 このように、「性質」をあらわすためのクラスをインターフェイスと呼ぶことも
あります。


*4意識してさけるべき

 その他に、注意深くつかわなければならないPythonの機能として、クラスメソッ
ドをインスタンスがフィールドとして遮蔽できる、というものもあります
 こんなことを連発していると、インスタンスメソッドがクラスで定義した時と同
じかどうか調べる必要がでてきます。
 一応、クラスメソッドにインスタンスを引数として渡す、という裏技的な方法が
ありますが、エレガントとは程遠いコードになりますので、そもそもメソッドをイ
ンスタンスフィールドで遮蔽するなどということは、しないことをお勧めします。