# 継承より合成ってなに?
名前空間をごっちゃにするから
継承とはコードを共通化することです。 コードを共通化することの危険性を動画にして、大変わかりやすく記載されています。 以下の動画の「共通モジュール」を親クラスに読み換えると雰囲気が掴みやすいかなと思います。
クソコード動画「共通化の罠」 pic.twitter.com/MM750CNXc2
— ミノ駆動 (@MinoDriven) May 12, 2019
「同じロジックのコードを2度以上書くな」と妄信する (opens new window)
同じようなパターンがプログラムの複数箇所に現れる場合、 それらを抽象化して一つの共通ロジックへのパラメータ渡しとして実装し、 それを複数箇所から呼び出すように実装すると、 プログラムコード量が小さくなり、 保守性が良くなったような気がするので、 未熟なプログラマが、なんでもかんでも共通ルーチン化しまくって、 非常に保守性の悪いプログラムにしてしまうことがある。
内容 | 継承 | 合成 |
---|---|---|
用途 | 小規模 | 大規模 |
規模が小さい、少人数の時は、システムが持つクラスの全体像が見渡せるので、 継承 をして 名前空間 をごっちゃにしても問題は大きくないかなと思います。 規模が大きい、大人数の時は、システムが持つクラスの全体像を見渡すことは、 難しいので継承より 合成 を使い 名前空間 をきっちりとわけた方が、 責任の分解点が明瞭になるかと思います。
内容 | 継承 | 合成 |
---|---|---|
実装 | 容易 | 面倒 |
名前空間 | ごっちゃ | わけられる |
継承は、簡単に実装を共有できます。 合成は、処理を 委譲 するコードを書かなければならず煩雑です。 継承は、簡単に実装できる反面、名前空間を継承した先のクラスと共有してしまい、責任の分解点が曖昧になります。 委譲 は、合成の実装が面倒ですが、名前空間をきっちりと分けることができます。
# 1. 合成と委譲
合成とは属性に持たせたい機能を持つオブジェクトを代入することです。 委譲とは合成で代入されたオブジェクトをメソッドから呼び出すことです。
# 継承 inheritance
class Team(list):
pass
team = Team()
team.append('諸葛亮')
team #
# 合成 composition
class Team:
def __init__(self):
self._list = []
# 委譲
def add(self, general):
self._list.append(generarl)
team = Team()
team.add('諸葛亮')
コンポジション(Composition)は、日本語で「混合物」を意味する単語である。あるクラスの機能を持つクラスのことを指す。 特定のクラスの機能を、自分が作るクラスにも持たせたい場合に、継承を使わずフィールドとしてそのクラスを持ち、 そのクラスのメソッドを呼び出すメソッドを持たせること。そうすることで、クラスに他のクラスの機能を組み込むことができる。
継承とコンポジションをどう使い分けるか - いぬごやねっと (opens new window)
# 1.1. 合成 composition
持たせたい機能を属性に代入することを合成と言います。
さて先頭にアンダーバーのついた不思議な属性名 _list
を指定していました。
これはなんでしょうか?
def __init__(self):
self._list = [] # <- ここ
Python のコーディング規約 PEP 8 では、クラスのメソッドの外から参照されたくない属性は、
先頭にアンダーバー _
をつけるように指定されています。
# インスタンス化した属性を直接みられたくない時に使います。
team = Team()
team._list.append('劉備')
こうやって書いてくださいね、という決まりごとなのですが、 詳細は、また追って説明させていただきます。
# 1.2. 委譲 deligation
合成でさえ面倒なのに、なぜわざわざ add という関数を使って呼び出しているのでしょうか? 直接、属性を操作してしまえば良いのではないでしょうか?
# 合成 composition
class Team:
def __init__(self):
self._list = []
team = Team()
team._list.append('諸葛亮') # <- これで良くない?
まだよくわからないのです、おそらく
結合度
が上がるのを避けるために行われるのだと思います。
結合度とは、ごくざっくり言えば、
.
で属性参照をするほど結合度が高いコードとみなされます。
なぜ、こう手間をかけてまで結合度を上げるのを避けたいのでしょうか?
add という関数を用意することで、 もし append に変更があった場合には add だけを変えれば良くなります。 直接 self._list.append を呼び出してしまうと、 呼び出している先、すべてのコードで変更が必要になります。
結合度については書籍「達人プログラマ 第5章柳に雪折れ無し 26 結合と最小化とデルメルの法則」で知りました。 この項目だけなら 7 ページほどです。抽象的な話ではなくちゃんと問題つきなので、オススメです。
# 1.3. まとめ
「合成」は属性に代入すること、 「委譲」は属性に代入されたメソッドを呼び出すことです。 最初、言葉だけ聞いたときはすごいことをするのかなと思ったのですが、 色々と調べてくうちに別にそんなに大したことはしていないことがわかりました。
オブジェクト指向における"委譲"に関する説明として,適切なものはどれか。
ア. あるオブジェクトに対して操作を適用したとき,関連するオブジェクトに対してもその操作が自動的に適用される仕組み
イ. あるオブジェクトに対する操作をその内部で他のオブジェクトに依頼する仕組み
ウ. 下位のクラスが上位のクラスの属性や操作を引き継ぐ仕組み
エ. 複数のオブジェクトを部分として用いて,新たな一つのオブジェクトを生成する仕組み
# 2. 継承は何故いけないのか?
# 2.1. 「オブジェクト指向」とは何か?
答え: 適切に名前空間を分けてコーディングをすること
オブジェクト指向の説明の仕方として、 現実世界のものに基づいてモデリングするという方法がありますが、 それは手段の1つであり必ずしも絶対ではなさそうでした。
オブジェクト指向とは、適切に名前空間にわけてコーディングすることだと言えます。 そうすることで責任分解点が明確になり、多人数でもコーディングしやすい環境が整うというわけです。 すなわち「カプセル化」です。
# 2.2. 「継承」とは何か?
答え: 親クラスと子クラスの名前空間を1つにまとめること
オブジェクト指向とは、適切に名前空間をわけること、という観点から考えた時に。 継承したコードについて考えてみると、同じ self という名前空間に対して処理をしていることに気づきます。 継承とは、せっかく分けた名前空間を親クラスと子クラスの名前空間を、ひとつに纏めていると言えます。
# 2.3. 「継承」は何故いけないのか?
# 2.3.1. 名前空間をごっちゃにしてしまう
継承は、せっかく適切に分けた名前空間をごっちゃにしてしまう行為です。 カプセル化を破ってしまいます。
継承はカプセル化を破る[Synder86]。
inheritance violates encapsulation[Synder86].
Effective Java - Item 18 composition over inheritance
極端な話、例えば、親クラスと子クラスを開発している人が別々だった場合、 継承をしてしまうと名前空間がごっちゃになり、責任の分解点が曖昧になってしまいます。
スコープに言い換えれば、継承してしまうことは、より広いスコープを使ってしまうことだとも言えます。
# 2.3.2. 機能の柔軟
「合成」の方が煩雑な反面、「継承」よりも柔軟に色々なことができるそうです。 この鎖で繋がったマリオのワンワン x 2 みたいなのはわかりやすかったです。
# 3. 継承はどのくらいいけないのか?
属性を順番に参照する機能は、「継承」を実現できるというメリットがあります。 しかし「継承」は、避けるべきだとされているらしいです。
せっかく「クラス変数とインスタンス変数ってなに?」 (opens new window) で 「継承」がどのように実装されているかを見てきたのに、何だか残念な感じですね。
継承よりも合成 - Wikipedia (opens new window)
オブジェクト指向プログラミング(OOP)における継承よりも合成(もしくは、合成による再利用の原則)とは クラスは多態性やコードの再利用性を、基底クラスもしくは親クラスからの継承ではなく、 合成によって(ほしいと思っている機能を実装した他のクラスのインスタンスを保持させることによって) 獲得しなければならないという原則である[2]。 これは例えば [3] のような影響のあるデザインパターンの本の中などで、よく言及される OOP の原則である。[3]。
composition over inheritance - Wikipedia
Composition over inheritance (or composite reuse principle) in object-oriented programming (OOP) is the principle that classes should achieve polymorphic behavior and code reuse by their composition (by containing instances of other classes that implement the desired functionality) rather than inheritance from a base or parent class.[2] This is an often-stated principle of OOP, such as in the influential book Design Patterns.[3]
最近生まれた Go や Rust にいたっては名前空間をごっちゃにしてしまう 継承という機能そのものを切ってしまっています。
RustやGoで継承がないのはそれが不要だから。 僕もそう思うのだけど、もっといえば使っちゃいけないと思っている。 理由は、継承は実装を伴い、共通処理を呼び出したりするのに使うけど、 メソッドが依存しあうことは、独立性の観点からダメ。 オブジェクトを結合点にすべき。実装共有するのは依存性を生む
— west_coder (@sntjnk88) March 18, 2018
# 3.1. Java
Java に関する記事です。 システム開発に伴う知見は Java に集まっていたりするのかなと感じたりします。
なぜ extends は悪なのか; 具象基底クラスをインターフェイスに置き換えてコードを改善する (opens new window)
Why extends is evil; Improve your code by replacing concrete base classes with interfacesかつて私は Java の開発者である James Gosling がゲストスピーカーに呼ばれた Java のユーザミーティングに参加しました。 印象的な Q&A セッションの間、誰かが彼に尋ねました。 「もし、 Java をもう一度開発し直せるなら、何を変えますか?」。彼は答えました「クラスを除きます。」
I once attended a Java user group meeting where James Gosling (Java's inventor) was the featured speaker. During the memorable Q&A session, someone asked him: "If you could do Java over again, what would you change?" "I'd leave out classes," he replied.笑いが収まった後、彼は本当の問題がクラスそのものではなく、 むしろ(extends の関係である)実装を伴う継承にあることを説明しました。 (implements の関係である)interface による継承が望ましい。 もしできるなら、実装継承は、いつも避けるべきだ。
After the laughter died down, he explained that the real problem wasn't classes per se, but rather implementation inheritance (the extends relationship). Interface inheritance (the implements relationship) is preferable. You should avoid implementation inheritance whenever possible.
「実装を伴う継承」と「interface による継承」については、 あとで Java と Python のサンプルコードを交えながら、 それぞれ解説させていただきます。 「実装を伴う継承」は、どのくらい避けた方の良いのでしょうか?
なぜ extends は悪なのか; 具象基底クラスをインターフェイスに置き換えてコードを改善する (opens new window)
Why extends is evil; Improve your code by replacing concrete base classes with interfacesJava の extends は悪だ; チャールズ・マンソンほどでないかもしれないが、 可能ならいつでも避けなければならないほど悪いものだ。 GoF のデザインパターンの本は、ページ数を割いて、 実装による継承 (extends) をインターフェイス (implements) による実装に置き換える方法について議論している。
The extends keyword is evil; maybe not at the Charles Manson level, but bad enough that it should be shunned whenever possible. The Gang of Four Design Patterns book discusses at length replacing implementation inheritance (extends) with interface inheritance (implements).良い設計者は、ほとんどのコードをインターフェイスについて書いていて、 具象ベースのクラスについては書いていない。 この記事は、なぜ設計者がそのような奇妙な習慣を持つのかを説明し、 2、3のインターフェイスベースのプログラミングの基礎について導入する。
Good designers write most of their code in terms of interfaces, not concrete base classes. This article describes why designers have such odd habits, and also introduces a few interface-based programming basics.
# 3.2. Rust
これは Rust の公式ドキュメントです。
継承は、近年、多くのプログラミング言語において、 プログラムの設計解決策としては軽んじられています。 というのも、しばしば必要以上にコードを共有してしまう危険性があるからです。 サブクラスは、 必ずしも親クラスの特徴を全て共有するべきではないのに、 継承ではそうなってしまうのです。 これにより、プログラムの設計の柔軟性を失わせることもあります。 また道理に合わなかったり、メソッドがサブクラスには適用されないために、 エラーを発生させるサブクラスのメソッドの呼び出しを引き起こす可能性が出てくるのです。 さらに、サブクラスに1つのクラスからだけ継承させる言語もあり、 さらにプログラムの設計の柔軟性が制限されます。
これらの理由により、継承ではなくトレイトオブジェクトを使用して Rust は異なるアプローチを取っています。 Rustにおいて、トレイトオブジェクトがどう多相性を可能にするかを見ましょう。
型システム、およびコード共有としての継承 - The Rust Programming Language (opens new window)
# 3.3. Go
こちらは Go の公式サイトを和訳したものです。更新が滞っていて、情報が古くなっている可能性があります。
# 4. 2種類の継承
# 4.1. 実装を伴う継承
# 4.1.1. 実装を伴う継承 - Python
「実装を伴う継承」とは Python の継承と同じです。
例えば下記の intimidate
脅す という実装されたコードを
Cat クラスと Dog クラスで継承しています。
def main():
animal_list = [Dog(), Cat()]
for animal in animal_list:
print(animal.sound())
print(animal.intimidate())
class Animal(object):
def intimidate(self):
return 'Gaoh!!!'
class Dog(Animal):
def sound(self):
return 'Woof, woof'
class Cat(Animal):
def sound(self):
return 'Meow, meow'
if __name__ == '__main__':
main()
...
Woof, woof
Gaoh!!!
Meow, meow
Gaoh!!!
>>>
# 4.1.2. 実装を伴う継承 - Java
このコードを Java で書き直してみます。
Java で継承するときは extends
を使います。
「extends の関係にある継承」とは、このことを指しています。
import java.util.ArrayList;
public class Main {
public static void main(String args[]) {
ArrayList<Animal> arrayList = new ArrayList<Animal>();
arrayList.add(new Dog());
arrayList.add(new Cat());
for(Animal animal : arrayList){
System.out.println(animal.sound());
System.out.println(animal.intimidate());
}
}
}
class Animal {
public String sound() {
return "Woh";
}
public String intimidate() {
return "Gaoh!!!";
}
}
class Dog extends Animal {
public String sound() {
return "Woof";
}
}
class Cat extends Animal {
public String sound() {
return "Meow";
}
}
実行結果は Python のものと同じになります。 paiza.io (opens new window) でコピペで実行できます。
Woof, woof
Gaoh!!!
Meow, meow
Gaoh!!!
Java はすべてのメソッドは、クラスに属していないといけない。 そのため main 関数が Main クラスにラップされています。
public class Main {
public static void main(String args[]) {
...
}
}
リストの要素の型についても明示する必要があります。 arrayList は Animal 型がはいる ArrayList ですよ、という宣言です。
ArrayList<Animal> arrayList = new ArrayList<Animal>();
# 4.2. interface による継承
「interface による継承」とはなんでしょうか?
# 4.2.1. interface による継承 - Python
正確ではないのですが Python で言えば継承しないことです。
Dog も Cat も実装を共有していません。
そして sound
, intimidate
と言う
関数の名前、interface だけが同じです。
def main():
animal_list = [Dog(), Cat()]
for animal in animal_list:
print(animal.sound())
print(animal.intimidate())
class Dog:
def intimidate(self):
return 'Gaoh!!!'
def sound(self):
return 'Woof, woof'
class Cat:
def intimidate(self):
return 'Gaoh!!!'
def sound(self):
return 'Meow, meow'
if __name__ == '__main__':
main()
# 4.2.2. interface による継承 - Java
Java のコードでは次のようになります。interface
とはなんでしょうか?
Python はメソッド名さえ同じであれば実行できますが、
Java のコードでは明示的に継承されたクラスしか、実行することができません。
そのため実装を伴わない場合は interface
を使います。
この interface
にはメソッド名だけ規定できます。
メソッドの中身を書くことができません。
またフィールドも持つことができません。
はじめて Java を習ったときは何が嬉しいのかさっぱりわかりませんでした。
extends
できるのは1つのクラスだけですが
interface
は幾つでももつことができます。
interface
は「戻り値の型」、「メソッド名」、「引数の型」、「引数名」の4つからなります。
言語によっては、たまにシグネチャ signatrue と呼ばれたりもします。
import java.util.ArrayList;
public class Main {
public static void main(String args[]) {
ArrayList<Animal> arrayList = new ArrayList<Animal>();
arrayList.add(new Dog());
arrayList.add(new Cat());
for(Animal animal : arrayList){
System.out.println(animal.sound());
System.out.println(animal.intimidate());
}
}
}
interface Animal {
public String sound();
public String intimidate();
}
class Dog implements Animal {
public String sound() {
return "Woof";
}
public String intimidate() {
return "Gaoh!!!";
}
}
class Cat implements Animal {
public String sound() {
return "Meow";
}
public String intimidate() {
return "Gaoh!!!";
}
}
Woof
Gaoh!!!
Meow
Gaoh!!!
# 4.3. mix-in による継承
mix-in の正確な定義はわからないのですが 自分だけではインスタンス化できないクラスを mix-in と呼ばれているのを目にします。 mix-in クラスを継承することは基本的には罪が軽いらしいです。
その温度感については Python では書籍「Effective Python」の「 項目 26 多重継承は mix-in ユーティリティクラスだけに使う」を参考にしてください。
Kotlin はリリース時から interface に実装を持たせることができる言語として誕生したらしいです。
上の方で散々実装を伴う継承はダメだ!と言っていたのですが、
Java 8 から default
句を使うことにより interface に実装を持たせられるようになりました。
# Java の default メソッド
あれだけ実装を伴う継承はダメって言ってたのに、いいのでしょうか?
interface が実装を持てるようになったということは、 実装の多重継承までできてしまうことを意味しています。
やはり書籍 Effective Java によると、使うにしても慎重になと言うことらしいです。
default メソッドは既存のインターフェイスにメソッドを追加できますが、 それをするのはとても大きなリスクが存在します。 While default methods make it possible to add methods to exsting interfaces, there is great risk to doing so.
Effective Java - Item 21: Design interface for posterity
以下は Ruby の記事ですが Mixin も、やはりまずいらしいです。
Ruby の Mixin について (opens new window)
Ruby の Mixin も継承の一種です。そのため、「悪い継承」のパターンは Mixin にも当てはまります。
例えば、
- 共通で使うメソッドを提供するMixinを定義する
といったことは避けましょう。Mixinは禁止くらいに思ってもいいかもしれません。
# 5. 名前空間をわける手段
(その1 合成と委譲)
Python では継承をしないこと、Java では interface
を使うことで、
「実装を伴う継承」を避けることができました。
しかし intimidate
というコードを
Dog クラスと Cat クラスで2回も実装しなければいけません。
果たしてこれでめでたしめでたしなのでしょうか?
これは面倒です。 実装による継承を避けつつ、同じ機能を持たせることはできないでしょうか。 大した話ではないのですが、よくある手段として「合成」と「委譲」があります。
上の方では Python のデリゲーションについて見ていただいたので、 ここでは Java と Kotlin のデリゲーションについて見ていただきたいと思います。
# 5.1. Python のデリゲーション
この例だと、正直あまり旨味を表現できないですね。関数ないしメソッドの
sound
の中身がもっと長ければ、なるほど。となるとは思うのです。
この例では合成と委譲をするための準備の方が大きすぎて、
逆に手間を取ってしまっています。
あと思ったのは、明示的に継承していないので、オブジェクトの性質みたいなのが、わかりづらいですね。 DogSound という機能を持っているから犬なのか、 CatSound という機能を持っているから猫、みたいな類推的な感じになります。 純粋に機能を渡しています。
def main():
animal_list = [Animal(DogSound()), Animal(CatSound())]
for animal in animal_list:
print(animal.sound())
print(animal.intimidate())
class Animal:
def __init__(self, sound):
self._sound = sound
def intimidate(self):
return 'Gaoh!!!'
def sound(self):
return self._sound.sound()
class DogSound:
def sound(self):
return 'Woof, woof'
class CatSound:
def sound(self):
return 'Meow, meow'
if __name__ == '__main__':
main()
...
Woof, woof
Gaoh!!!
Meow, meow
Gaoh!!!
>>>
# 5.2. Java のデリゲーションパターン
Delegation Pattern (opens new window) なる言葉があります。 デリゲーション先のオブジェクトのクラスにインターフェイスを継承させる。
便利な機能というわけではないですが、こういうやり方があるらしいです。 さきほどは Animal という実体を持ったクラスを継承していましたが、 今度は Sound という実体を持った機能を合成します。
なんで Java の話するの?って感じですが、 やはり経験値が Java には溜まっています。 デリゲーションパターンという形で明確に、 ノウハウを教えてくれるのは Java だけ!
書籍「Effective Java」とかも結構いい。 正直「Effective Python」よりもいい。 まず、すごく丁寧。 Java の案件には絶対に関わりたくないけど、 Effective Java を読むために Java を習得してる気がする。
Effective Python もいいんだけど、 結構さっぱり書かれすぎてて、何が言いたいのかを4、5回くらい宇宙猫にならないと理解できない。 理解できないときはしばらくして、あ、これ Effective Python に書いてあったやつだ!ってなるから恐ろしいのですが。
その辺の絡みもあって 型アノテーション を紹介させていただきました。 型が明示されたコードをいきなり見せられると、え?って感じになるので。
デリゲーション先が子クラスでデリゲーション元が親クラスです。 Online Java compiler - Paiza (opens new window) に コピペで実行できます。
import java.util.ArrayList;
public class Main {
public static void main(String args[]) {
ArrayList<Animal> arrayList = new ArrayList<Animal>();
arrayList.add(new Animal(new DogSound()));
arrayList.add(new Animal(new CatSound()));
for(Animal animal : arrayList){
System.out.println(animal.sound());
System.out.println(animal.intimidate());
}
}
}
class Animal {
protected Sound _sound;
Animal(Sound sound) {
this._sound = sound;
}
public String intimidate() {
return "Gaoh!!!";
}
public String sound() {
return this._sound.sound();
}
}
interface Sound {
public String sound();
}
class DogSound implements Sound {
public String sound() {
return "Woof";
}
}
class CatSound implements Sound {
public String sound() {
return "Meow";
}
}
# 5.3. Kotlin のデリゲーション機能
デリゲーションは、面倒です。 ちなみに Kotlin Playground (opens new window) にコピペで実行できます。
fun main(args: Array<String>) {
var animalList = mutableListOf(Animal(DogSound()), Animal(CatSound()))
for(animal in animalList){
println(animal.sound())
println(animal.intimidate())
}
}
class Animal(sound: Sound) {
private val _sound: Sound
init{
this._sound = sound
}
fun intimidate(): String {
return "Gaoh!!!"
}
fun sound(): String {
return this._sound.sound()
}
}
interface Sound {
fun sound(): String
}
class DogSound: Sound {
override fun sound(): String {
return "Woof"
}
}
class CatSound: Sound {
override fun sound(): String {
return "Meow"
}
}
デリゲーションパターンはよく使われるものです。 そのためデリゲーションをするための 糖衣構文 (opens new window) を Kotlin は持っています。 逆に短すぎてよくわからない笑
fun main(args: Array<String>) {
var animalList = mutableListOf(Animal(DogSound()), Animal(CatSound()))
for(animal in animalList){
println(animal.sound())
println(animal.intimidate())
}
}
class Animal(sound: Sound): Sound by sound {
fun intimidate(): String {
return "Gaoh!!!"
}
}
interface Sound {
fun sound(): String
}
class DogSound: Sound {
override fun sound(): String {
return "Woof"
}
}
class CatSound: Sound {
override fun sound(): String {
return "Meow"
}
}
# 6. 名前空間をわける手段
(その2 命名規則とアクセス修飾子)
継承関係にあるクラス間で名前空間を参照できないようにすれば、良いわけです。 ここではプライベートとプロテクテッドの両方について説明します。
# 6.1. private と protected
# (1) protected と private
多人数で開発するときは、勝手に使って欲しくない属性、メソッドがあります。
例えば、自分専用に頻繁に機能を書き換えたりするメソッド、その場しのぎで作ったすぐに消すメソッドが想定されます。
そう言った言わば自分だけで使いたい属性、メソッドには先頭にアンダーバー _
をつけます。
公開されていない識別子は、さらに大きく2つに分けられます。 継承した子クラスのメソッドからなら参照しても良い属性と、 継承した子クラスのメッソドからでも参照してはいけない属性です。 アンダーバーが1つのものと2つのものです。
# グローバルスコープ
# グローバル変数
_val1 = 1 # protected
# __val2 = 10 # private
class Person(object):
# ローカルスコープ(クラス)
# ローカル変数
# クラス変数
_val3 = 100 # protected
__val4 = 1000 # private
def func(self):
# ローカルスコープ(関数)
# ローカル変数
# インスタンス変数
self._val5 = 10000 # protected
self.__val6 = 100000 # private
Python | Java | Kotlin | |
---|---|---|---|
使っても良い | public | public | public |
使っても良い(子クラスからなら) | protected | protected | private |
使ってはいけない | private | private | protected |
モジュールレベルは private は存在しませんが、 いちおうわかりやすくするために、コメントアウトして記載しておきます。 なぜグローバル変数に private が存在しないかと言えば、 それはモジュールがクラスとは違い継承できないからです。
アンダーバーが1つ _
の場合、
すなわち継承した他のクラスやインスタンスからなら見える属性を protected と表現されることがあります。
アンダーバーが2つ __
の場合、
すなわち継承した他のクラスやインスタンスからは
見えないより重たい制限のかけられた属性を private と表現されることがあります。
Java も同じく public, protected, private なのですが、というかおそらく この慣習は Java から Python に輸入されたと思います。 ただ Kotlin は一部逆になっていて public, private, protected となっています。 確かに private よりは protected の方が制限が強そうな語感はありますが。
ただ、標準ライブラリのコードなどを見ているとアンダーバー2つ __
の private な属性をあまり、
見かけたことがありません。
書籍「Effective Python」の 「Item 27: プライベート属性よりもパブリック属性を使おう」で
そう言ったところの温度感について触れられています。
# (2) 機能と規約
# グローバルスコープ
# グローバル変数
_val1 = 1 # protected
# __val2 = 10 # private
"""
# 参照
できない
# 説明
モジュールを import しても
この変数 _val1, __val2 は import されません。
"""
class Person(object):
# ローカルスコープ(クラス)
# ローカル変数
# クラス変数
_val3 = 100 # protected
"""
# 参照
obj._val3
# 機能
無。命名規則です。
"""
__val4 = 1000 # private
"""
# 参照
Person._Person__val3
# 機能
Person.__val4 では参照できない。
Person._Person__val4 として参照します。
# いつ使うの?
他のクラスから継承されたときに
名前が上書きされてしまうのを避けるため。
"""
def func(self):
# ローカルスコープ(関数)
# ローカル変数
# インスタンス変数
self._val5 = 1000
self.__val6 = 10000
"""
# 参照
# 機能
# いつ使うの?
ローカルスコープ(クラス)で定義されたクラス変数と同じ
"""
継承の設計 - PEP 8 (opens new window)
クラスのメソッドやインスタンス変数 (まとめて "属性" といいます) を 公開するかどうかをいつも決めるようにしましょう。
よくわからないなら、公開しないでおきます。 なぜなら、公開されている属性を非公開にすることよりも、 非公開の属性を公開することの方がずっと簡単だからです。
公開されている(public)属性に対して、開発者が後方互換性を壊す変更をしないことを期待します。 公開されていない(non-public)属性は、サードパーティに使われてることを意図していないものです。 つまり、非公開の属性に変更されない保証はありませんし、削除されない保証すらありません。
項番 | レベル | 命名 規則 | 機能 規約 | 参照の仕方 |
---|---|---|---|---|
1 | モジュール | _val1 | 機能 | 参照できない |
2 | モジュール | __val2 | 機能 | 参照できない |
3 | クラス | _val3 | 規約 | obj._val3 |
4 | クラス | __val4 | 機能 | obj._クラス名__val4 |
5 | インスタンス | _val5 | 規約 | obj._val5 |
6 | インスタンス | __val6 | 機能 | obj._クラス名__val6 |
- 2.3.2. 予約済みの識別子種 (reserved classes of identifiers) (opens new window)
- 実践されている命名方法 (opens new window)
ここでの理解のポイントは、
_
が単なる PEP 8 という「コーディング規約」なのに対して
__
が「文法」でもあることです。
このことを区別しておくと理解が進みます。
ただ、ここで厄介なのは「機能」と「規約」がごっちゃになっていることです。
「機能」についてはこちらで記載されています。
「規約」、PEP 8 についてはこちらに記載されています。
# (3) 純粋なローカル変数について
蛇足ではありますが、純粋な関数の中のローカル変数は覗くことはできませが、
アンダーバー _
をつけるようなことはしません。
class Cls:
def func(self):
# ローカルスコープ(関数)
# ローカル変数
val7 = 100000
"""
# 参照
参照できない
# 機能
参照できない
# 説明
_val7 = 100000
とはしません。
1つ目の理由は、
関数内のローカル変数なら、
スコープが小さいので大抵自明だから。
2つ目の理由は、
関数内のローカル変数は
func.val7 という様に外部から参照できないから。
ただ関数内で
func.val7 = 10
として関数の属性に値を代入できて
関数外から参照することができたりします。
こんな機能があったとは...
"""
# (4) まとめ
2つのダブルスコア __
を先頭に付与すると、参照時するために自動的に名前が変更されます。
マングリング化されます。
つまりクラスやそのクラスに属するインスタンス専用の 名前空間 を確保することができます。
# 6.2. システムで定義された名前
__bases__, __class__, __dict__ とかアンダースコア _ で括られた変数が出てきました。 これは何でしょうか?システムで定義された (system-defined) 名前です。 Python が使っているか、あるいは Python が使い方を指定した変数です。 __init__ なんかもこれの仲間に入ります。 クラス変数を 特殊属性 special attribute (opens new window)、 メソッドを 特殊メソッド special method (opens new window) と呼びます。
__double_leading_and_trailing_underscore__: (opens new window)
特別な意味を持つ変数名あるいは属性名です。 例えば、__init__, __import__ or __file__ が該当します。 このように二重のアンダースコア __ で囲まれた変数名、属性名は、公式のドキュメントで定められた通り使い、 自分で定義たりしないでください。
__double_leading_and_trailing_underscore__:
"magic" objects or attributes that live in user-controlled namespaces. E.g. __init__, __import__ or __file__. Never invent such names; only use them as documented.
__*__ (opens new window)
システムで定義された (system-defined) 名前です。 これらの名前はインタプリタと (標準ライブラリを含む) 実装上で定義されています; ... (中略) ... このドキュメントで明記されている用法に従わない、 あらゆる __*__ の名前は、いかなる文脈における利用でも、 警告無く損害を引き起こすことがあります。
# なんで使っちゃ行けないの?
__*__ という名前を勝手に使わないでね、という命名規則がなぜあるのでしょうか? 例えば、使っていない名前があるなら、使いたくもなります。__*__ という名前を勝手に使われてしまうと、 Python が、新しくシステムが使う変数やメソッドを定義しようと思ったときに、問題が起こります。 Python がバージョンアップするときに、新しい変数やメソッドを追加できなくなってしまうからです。
例えば Python 3.5 では新しく __matmul__, __rmatmul__, __imatmul__ が追加されました。 これらの属性をあるコードが勝手に別の用途で使っていたら Python 3.5 では、そのコードが動かなくなってしまいます。 つまり、Python 3.5 は、Python 3.4 以前のバージョンのコードに対して、後方互換性を失ってしまうことになります。
Python はなぜいくつかの処理にメソッドではなく、関数を使っているのですか?(例えば len(list) とか?) (opens new window)
Why does Python use methods for some functionality (e.g. list.index()) but functions for other (e.g. len(list))?私 (Guido van Rossum) が説明すると約束した2つ目の Python の設計背景は、 なぜ特殊メソッドの見た目を単純に special とはせずに __special__ したのかだ。
The second bit of Python rationale I promised to explain is the reason why I chose special methods to look __special__ and not merely special.私は、多くのクラスがメソッドをオーバーライドするだろうと考えた。 例えば、一般的なメソッド(例えば __add__ や __getitem__)、 あまり一般的でないメソッド (例えば pickle の __reduce__、これは長いこと C 言語で一切サポートされていませんでした。)
I was anticipating lots of operations that classes might want to override, some standard (e.g. __add__ or __getitem__), some not so standard (e.g. pickle‘s __reduce__ for a long time had no support in C code at all).私はこれらの特殊メソッドには、一般的なメソッド名を使って欲しくなかった。 なぜなら、すでに設計されたクラス、またはこれらの全ての特殊メソッドを覚えていないユーザによって書かれたクラスが、 意図せずメソッドをオーバーライドしてしやすく、結果として悲劇的な結果を引き起こす可能性を秘めているからだ ("すでに設計されたクラス" というのは、特殊メソッドを追加した時に、 古いコードで同じ特殊メソッド名が既に使われてしまうと、後方互換性が失われることを指しているのかな..) 。
I didn’t want these special operations to use ordinary method names, because then pre-existing classes, or classes written by users without an encyclopedic memory for all the special methods, would be liable to accidentally define operations they didn’t mean to implement, with possibly disastrous consequences.
言い換えれば Python インタープリタのための名前空間が確保されていると言えます。
PEP 3114 - iterator.next() から iterator.__next__() への名称変更 (opens new window)
PEP 3114 - Renaming iterator.next() to iterator.__next__()Python の言語仕様の一部となっている変数、属性にダブルアンダースコアを付加するようにすれば、 Python の言語仕様の一部となっている変数、属性のための 名前空間 を作ることができます。 そのためプログラマは、気づかないうちに Python の言語仕様上定義された名前を上書きしてしまうことを気にすることなく、 アルファベットから始まる名前を変数、属性、そしてメソッドに使うことができます。 (たとえアルファベットから書き始めても、class, import などの予約語と衝突してしまう可能性は残りますが、 予約語を変数名に使用した場合にはすぐに syntax error が返されます。)
The use of double underscores creates a separate namespace for names that are part of the Python language definition, so that programmers are free to create variables, attributes, and methods that start with letters, without fear of silently colliding with names that have a language-defined purpose. (Colliding with reserved keywords is still a concern, but at least this will immediately yield a syntax error.)
# 7. なんで多重継承を許してしまったのだろう?
Java では意図的に実装を伴う多重継承を切っています。
1つのクラスが実装を伴う継承をできるのは extends できるのは1つのクラスだけです。 Python で言えば親クラスは1つだけです。
それに対して「インターフェイスの継承」は、実装を伴わなければ、 複数のクラスを親クラスに持つことができます。
Python では親クラスを複数継承できます。 どういうことかと言えば Python ではこの「実装を伴う継承」よりもさらに極悪な 「実装を伴う多重継承」を許してしまっているということです。 super は、そのための機能です。
Python は、可読性をかなり重視して、設計をしているにも関わらず。 これは一体、どういうことでしょうか?
なぜなら多重継承は問題点が多いと思われたためである。
- 継承関係が複雑になるため全体の把握が困難になる。
- 名前の衝突。同じ名前を複数の基底クラスがそれぞれ別の意味で用いていた場合、 その両方を派生クラスでオーバーライドするのが困難。
- 処理系の実装が複雑になってしまう。
- ... 省略 ...
しかしながら多重継承を使う方が直感的になる場合もあるとの主張もあり、どちらが正しいとは言えない状況である。
継承 (プログラミング) - Wikipedia (opens new window)
# 7.1. わかっていなかったから
わかっていながらなんで「実装による継承」よりも、 さらに極悪な「実装による多重継承」なんていう機能を許してしまったのかというと、 完全に推測ですが恐らく、わかっていなかったからです。
私 (Guido van Rossum) は、 多くのクラスがメソッドをオーバーライドするだろうと考えた。
I was anticipating lots of operations that classes might want to override, some standard
Why does Python use methods for some functionality (e.g. list.index()) but functions for other (e.g. len(list))? (opens new window)
1995 年に Java が生まれて多くのシステムが開発され、 どうも「実装による継承」はあかんらしいという知見が得られる前の 1991 年に Python は生まれました。 Python は、日本では最近人気を得た言語ですが、実際には Java よりも Python の方が、ずっとおじいちゃんな言語です。
Pythonは死にかけの言語なのか
いいえ、そうではありません。」Pythonは”比較的に”古い言語です。 最初に登場したのは1990年代初期です(仮に1991年としましょう)。 そして、他のプログラミング言語と同様に、Pythonも正しい選択をすることと、 妥協することが必要でした。 どんなプログラミング言語にもそれぞれのクセがあります。 より最近の言語は過去の失敗から学んでいる傾向にあります。 これはいい傾向です。
Pythonや機械学習、そして言語の競争について - POSTD (opens new window)
実は Python のオブジェクト指向は、その大部分が後付けされた機能です。 以下の記事は、Guido が Python の開発経緯について話してくれている記事になります。
にわかには信じられない人もいると思うが、 オランダ国立情報数学研究所で開発が行われていた最初の一年の間、 Python はクラスをサポートしておらず、 最初の公開リリースの前にオブジェクト指向をサポートするようになった。 どのようにクラスが追加されたかという過去の経緯を理解してもらう手助けになると思うので、 現在の Python がどのようにクラスをサポートしているのか、という点から話を始めようと思う。
ユーザ定義クラスのサポートの追加 - The History of Python.jp (opens new window)
# 7.2. そんなに問題ではないから
ちゃぶ台返しになりますが。そうではないという意見もあります。
Lennart Regebro は非常に経験豊富な Pythonista で、 本書のテクニカルレビューアの一人でもあるのですが、 Django の mixin ビュー階層の設計は複雑だと考えています。 しかし、彼は次のようにも述べています。
多重継承の危険性と害悪はかなり誇張されていて、実際のところ、 私はこれまでにそれほど大きな問題を経験したことはありません。
結局、多重継承の使い方や、自分のプロジェクトで多重継承をまったく使わないようにするかについては、 さまざまな意見があります。しかし、使おうとしているフレームワークがその選択を強制してくることもあり、 他にチョイスがないこともあります。
Fluent Python - 12.4.9 Tkinter―善玉、悪玉、卑劣なやつ (opens new window)
実際 Python 3.4 で新しく標準ライブラリに追加された pathlib も、 図を見る限り多重継承を使い実装されているようです。
pathlib の経緯とかは、この記事がわかりやすかったです。 いつの間にか、パスをオブジェクトで取り扱うなんていうビッグウェーブが来てたんですね。
# 8. まとめ
ここまで以下のような流れで見てきました。
これが絶対という明確な基準がわかったわけではないのですが、 以上のことを踏まえると規模が小さいうちは、 一人が全体を把握できるようなうちは、別に使ってもいいのかなという気がします。
同じプログラマの管理下に置かれたサブクラスとスーパークラスを持つ パッケージの中であれば、継承を使うことは安全です。
It issafe to use inheritance within a package, where the subclass and the superclass implementations are under the control of the same programmer.
Effective Java - Item 18: Favor composition over inheritance
ただ、規模が大きくなって堅牢なものを作ろうとすると、 切らざる得なくなってるという感覚かなと思います。 規模が大きくなり過ぎて一人では把握しきれなくなったら、 合成にしなければならないかなと思います。 多人数で開発している時に「実装を伴う継承」をして名前空間をごっちゃにしたら、 収集がつかなくなりそうな気がします。
それはちょうど、動的型付けで型なんか明示しなくてもいいやろって言ってたけど、 やっぱり取り扱うものの規模が大きくなると、アノテーションとしての型が採用されたように。
これで最後になります。 ここまでお付き合いいただいた方がいらっしゃいましたら、幸いでございます。 本当にありがとうございました。