Pythonのオブジェクト指向プログラミングを完全理解 (1)
オブジェクト指向
1. オブジェクト指向の起源
2003年チューリング賞の受賞者アラン・ケイさんはよくオブジェクト指向プログラミングの父と称されます。ご本人も憚ることなく、幾度、公の場で発明権を宣言しています。しかし、ケイさんは「C++」や「Java」などの現代のオブジェクト指向言語を蔑ろにしています。これらの言語は「Simula 67」という言語を受け継いだもので、私が作った「Smalltalk」と関係ないのだとケイさんは考えています。
オブジェクト指向という名称は確かにアラン・ケイさんに由来するものです。しかし、C++とJavaで使われている現代のオブジェクト指向は当初のと結構違います。ケイさん自身もこれらの言語を後継者として認めないです。では、ケイさん曰くC++とJavaの親であるSimula 67という言語はどんな言語でしょうか。ここで、簡単なサンプルコードを見てみましょう。
Class Rectangle (Width, Height); Real Width, Height;
! Class with two parameters;
Begin
Real Area, Perimeter; ! Attributes;
Procedure Update; ! Methods (Can be Virtual);
Begin
Area := Width * Height;
Perimeter := 2*(Width + Height)
End of Update;
Boolean Procedure IsSquare;
IsSquare := Width=Height;
Update; ! Life of rectangle started at creation;
OutText("Rectangle created: "); OutFix(Width,2,6);
OutFix(Height,2,6); OutImage
End of Rectangle;
2つの変数を持つclass
ですね。文法は分からないですが、コメントを見てどういうものかは大体見当がつくでしょう。Simula 67は名前の通り、1967年に発表され、1973年にリリースされたプログラミング言語です。それに対して、Smalltalkは1975年に最初のバージョン(Smalltalk-72)が発表され、1980年代にリリースされた言語です。
class
があればオブジェクト指向プログラミングだというわけではないですが、Simula 67のclass
は「インスタンス」、「継承」、「メソッド」 や「late binding」までサポートしています。Simula 67は間違いなくオブジェクト指向系統の言語です。
しかし、Simula 67のオブジェクト指向の設計もオリジナルなものではないです。1965年、アントニー・ホーアさん(1980年チューリング賞受賞者)はある論文を発表しました。その論文に、record class
という概念が提出されました。ホーアさんは、ALGOLという言語でサンプルを書きました。
record class person;
begin integer date of birth;
Boolean male;
reference father, mother, youngest offspring, elder sbling (person)
end;
複合的なデータ型で、C言語の構造体と似ていますね。
そして1966年に、あるサマースクールで、ホーアさんはクリステン・ニガードさんとオルヨハン・ダールさんと出会いました。後ほどSimula 67を作ったのはこの2人なのです。ホーアさんはrecord class
のアイデアを2人に共有しました。ダールさんの話によると、その時ホーアさんはすでに「継承」の概念も思いつき、2人に教えました。そして、2001年に、クリステン・ニガードさんとオルヨハン・ダールさんはオブジェクト指向への貢献によりチューリング賞を受賞しました。アラン・ケイさんよりも2年早かったですね。
Simula 67について紹介しました。Simula 67は世界初のオブジェクト指向系統の言語ということも理解していただけたと思います。では、ケイさんが作ったSmalltalkは偽物ですか?結論から言うと、そうでもないです。Lisp言語の「Everything is a list」に対して、Smalltalkは初めて「Everything is an object(全てがオブジェクト)」という概念を作りました。更に、Smalltalkは演算子も含め式はすべてオブジェクトに対する「メッセージ」と解釈しています。Smalltalkこそが、オブジェクト指向に追い風を吹かせたプログラミング言語です。1980年代、Smalltalkのおかげで、オブジェクト指向プログラミング言語が輩出していました。その中に、今でもまだ健在しているC++などもあります。更に関数型プログラミング言語の元祖であるLisp陣営も「Common Lisp Object System」を手に持ち加勢していました。
最後に、1996年に現代のオブジェクト指向プログラミングパラダイムの最高峰であるJavaが発表されました。これが、オブジェクト指向史上の大きなマイルストーンとなる事件です。Java自体はオブジェクト指向において何も発明していないですが、今までの優秀な概念を吸収し、更にJVMの優れたマルチプラットフォーム性能とGCを備え合わせ、今でも世界TOP3にランクインするプログラミング言語となっています。
2. オブジェクト指向の特徴
オブジェクト指向の起源について紹介しました。しかし、そもそもオブジェクト指向とは何でしょうか?本題に入る前に、まず簡単な例を使って説明したいと思います。
オブジェクト指向はよくプロセス指向(手続き型プログラミングとも言う)と比較されます。下のコードはプロセス指向とオブジェクト指向の形式を表したものになります。
a = 0
# a+3の機能を実現したい
# プロセス指向
sum(a, 3)
# オブジェクト指向
a.sum(3)
ただ書き方が違うだけじゃんと思うかもしれません。実は、オブジェクト指向プログラミングはコードのロジックを明瞭化することができます。そして、その威力はプログラムが大きければ大きいほど発揮されるものです。続いて、上記のコードの違いを詳しく見ていきましょう。
1. 構文
関数呼び出しの構文を語順の考え方で解釈することができます。
- プロセス指向は通常、
動詞(主語, 目的語)
という構造になっています。動詞がメインで、主語と目的語は引数として渡されます。 - オブジェクト指向は、SVO型、いわゆる
主語, 動詞, 目的語
の構造になっています。つまり、主語がメインになります。そして、主語がある動詞を呼び出して、目的語を引数として渡しています。日本語ですと、動詞が後に来るので、SVO型はしっくり来ないかもしれませんが、英語、ヨーロッパの多くの言語、中国語などはSVO型に準ずる言語なので、オブジェクト指向の方式は意味合い的には自然になります。
2. 定義方式
- プロセス指向は、
sum
という2つの引数を受け取り、その和をreturn
する関数を定義します。 - オブジェクト指向は、やや複雑で、まず
class
を定義します。そのclass
の中に、様々なメソッド(関数と理解しても良い)を定義します。そして、class
のインスタンスを作成し、そのインスタンスからメソッドを呼び出します。 - 上の例では、
a
は整数で、int
というclass
のインスタンスになります。整数のインスタンスは足し算や引き算のようなint
のメソッドが使えます。 - オブジェクト指向は変数を自動的に分類します。全ての変数はオブジェクトであり、ある
class
に属し、使えるメソッドも決まっています。例えば、文字列が来たら、どういうメソッドが使えるかはclass str
を見れば分かります。
3. 呼び出し方式
実践では、複数のオブジェクトに同じ処理をしたい時:
- プロセス指向は、1個または複数の関数を作って、全てのオブジェクトに対して関数を適用すれば実現できます。
- オブジェクト指向は、
class
とそのclass
のメソッドを定義し、全てのオブジェクトに対して、class
のインスタンスを作り、そのインスタンスからメソッドを呼び出します。
このような処理が多くなると
- プロセス指向は、たくさんの関数が定義され、関数の中に関数呼び出ししている可能性もあり、構造がどんどん不明瞭になります。そして、あるオブジェクトが来たら、関数で処理できるかどうかは中身を見ないと分からない場合もあります。
- オブジェクト指向は、
class
単位でメソッドをまとめて管理し、オブジェクトの使えるメソッドは自明です。
次に、オブジェクト指向のメリットをPythonのコード例を通して、少し詳しく見ていきます。
2-1. インターフェースの統一と管理
鳥、犬、魚の3つのclass
を定義します。
class Bird:
def __init__(self, name):
self.name = name
def move(self):
print("The bird named {} is flying".format(self.name))
class Dog:
def __init__(self, name):
self.name = name
def move(self):
print("The dog named {} is running".format(self.name))
class Fish:
def __init__(self, name):
self.name = name
def move(self):
print("The fish named {} is swimming".format(self.name))
インスタンスを作ります。
bob = Bird("Bob")
john = Bird("John")
david = Dog("David")
fabian = Fish("Fabian")
次に、全てのインスタンスのmove
メソッドを呼び出します。
bob.move()
john.move()
david.move()
fabian.move()
実行結果:
The bird named Bob is flying
The bird named John is flying
The dog named David is running
The fish named Fabian is swimming
インスタンスを作成する時、パラメータを渡す必要があります。このパラメータはオブジェクトが他のオブジェクトと区別するためのデータとなります。例えば、bob
というオブジェクトのname
はBob
で、john
のname
はJohn
なので、同じclass
から作成されたインスタンスにもかかわらず、違うオブジェクトになり、同じメソッドを実行しても結果が異なります。
また、違うclass
のmove
メソッドは、違う結果を出力しています。例えば、Bird
のmove
はThe bird named...
を出力し、Dog
はThe dog named...
を出力します。move
メソッドは「移動」という意味で、各動物class
は移動できるので、同じmove
として実装することで、インターフェースが統一していて記憶しやすくなります。
プロセス指向で実装すると、以下のような感じになるかもしれません。
def move_bird(name):
print("The bird named {} is flying".format(name))
def move_dog(name):
print("The dog named {} is runing".format(name))
def move_fish(name):
print("The fish named {} is swimming".format(name))
bob = "Bob"
john = "John"
david = "David"
fabian = "Fabian"
move_bird(bob)
move_bird(john)
move_dog(david)
move_fish(fabian)
bob
というオブジェクトが来たら、それが「鳥」なのか「犬」なのかをまず明確にしないと、move_bird
とmove_dog
のどれにするかが決められません。実際のプログラムではmove
だけではなく、数十種類の処理関数を実装するのが普通です。関数が多くなると、変数との対応関係を明確にするのが極めて難しくなります。また、これらの関数は内部で他の関数を呼び出している可能性もあり、この関数を他のプログラムで再利用する時に、内部で使われている関数も全部見つけ出して、移行する必要があります。
オブジェクト指向は変数を使って、class
からインスタンスを作成し、どのメソッドが使えるかはclass
を見れば分かります。そして、class
として抽象化することで、同じ文脈の関数が固まり、管理しやすくなります。
2-2. カプセル化
オブジェクト指向は関数とデータを一緒に束ねてくれるので、同じ変数(データ)をたくさんの関数で処理したい時はとても便利です。
class Person:
def __init__(self, name, age, height):
self.name = name
self.age = age
self.height = height
def describe(self):
print("name: {}; age: {}; height: {}".format(self.name, self.age, self.height))
def introduce(self):
print("My name is {}, and height is {}, and age is {}. ".format(self.name, self.height, self.age))
bob = Person("Bob", 24, 170)
mary = Person("Mary", 10, 160)
bob.describe()
bob.introduce()
mary.describe()
mary.introduce()
実行結果:
name: Bob; age: 24; height: 170
My name is Bob, and height is 170, and age is 24.
name: Mary; age: 10; height: 160
My name is Mary, and height is 160, and age is 10.
上記の処理をプロセス指向で実装すると、以下の2通りの方法があります。1つはそのまま引数として渡す方法です。
def describe(name, age, height):
print("name is {}, age is {}, height is {}".format(name, age, height))
def introduce(name, age, height):
print("My name is {}, and height is {}, and age is {}. ".format(name, height, age))
describe("Bob", 24, 170)
describe("Mary", 20, 160)
introduce("Bob", 24, 170)
introduce("Mary", 20, 160)
上記の方法は毎回同じ引数を渡す必要があり、引数が多くなると、非常に面倒です。もう1つは毎回引数を渡す必要のない方法です。
bob = dict(name='Bob', age=24, height=170)
mary = dict(name='Mary', age=20, height=160)
def introduce(**kwargs):
print("My name is {name}, and height is {age}, and age is {height}. ".format(**kwargs))
def describe(**kwargs):
print("Description: name is {name}, age is {age}, height is {height}".format(**kwargs))
introduce(**bob)
describe(**bob)
introduce(**mary)
describe(**mary)
この方法は引数を辞書で格納して、引数として辞書をアンパックして渡すようにしています。しかし、もし辞書の中にname, age, height
の3つのキーが存在しないと、エラーを起こしてしまいます。
このように、プロセス指向と比べて、オブジェクト指向は処理とデータをまとめてカプセル化してくれるので、コードのロジックが綺麗になりがちです。
2-3. オブジェクトの動的操作
オブジェクトの動的な一連の動作の実現は、プロセス指向には不向きです。
class Individual:
def __init__(self, energy=10):
self.energy = energy
def eat_fruit(self):
self.energy += 1
return self
def eat_meat(self):
self.energy += 2
return self
def run(self):
self.energy -= 3
return self
anyone = Individual()
print("energy: {}".format(anyone.energy))
anyone.eat_meat()
print("energy after eat_meat: {}".format(anyone.energy))
anyone.eat_fruit()
print("energy after eat_fruit: {}".format(anyone.energy))
anyone.run()
print("energy after run: {}".format(anyone.energy))
anyone.eat_meat().run()
print("energy after eat_meat and run: {}".format(anyone.energy))
実行結果:
energy: 10
energy after eat_meat: 12
energy after eat_fruit: 13
energy after run: 10
energy after eat_meat and run: 9
上記の「個体」のclass
は「エネルギー」という内部状態パラメータと「果物を食べる」、「肉を食べる」、「走る」の3つメソッドを持ちます。次に、さらに細分化した「男の子」と「女の子」の2つのclass
を定義します。
class Boy(Individual):
def daily_activity(self):
self.eat_meat().eat_meat().run().eat_meat().eat_fruit().run().eat_meat()
print("boy's daily energy: {}".format(self.energy))
class Girl(Individual):
def daily_activity(self):
self.eat_meat().eat_fruit()
print("girl's daily energy: {}".format(self.energy))
bob = Boy()
bob.daily_activity()
mary = Girl()
mary.daily_activity()
実行結果:
boy's daily energy: 13
girl's daily energy: 13
上記の処理をプロセス指向で実装すると、オブジェクトごとに、専用のenergy
という変数と、それぞれのenergy
を処理する関数を定義する必要があり、冗長になるのが避けられないです。
また、主語, 動詞, 目的語
の構造は比較的に理解しやすいです。上記の例では、まずeat_meat()
、次にrun()
という一連の動作が永遠に続いても理解できます。プロセス指向で実現するとboy_energy = eat_meat(boy_energy); boy_energy = run(boy_energy);...
のような長文になるか、eat_meat(run(boy_energy))
のような階層構造になるので、理解しにくくなるでしょう。
3. オブジェクト指向に関する概念
オブジェクト指向の特徴について簡単に紹介しました。ここからは少し高度な内容に入ります。オブジェクト指向には様々な概念がありまして、これらを説明しようと思います。
3-1. クラス
クラスは、同じ属性(変数、データ)と処理(メソッド、関数)を持つオブジェクトの設計図です。クラスは自身から生成されるオブジェクトの共通の属性と処理を定義します。プロセス指向言語では、変数は型によって分類されるのに対して、オブジェクト指向言語では、変数はクラスによって分類されます。そして、オブジェクト指向言語の型自体もクラスになっています。
ちなみに、Python 2には古いクラスと新しいクラスがあり、それぞれは以下のようになります。
class oldStyleClass: # inherits from 'type'
pass
class newStyleClass(object): # explicitly inherits from 'object'
pass
Python 3になると、全てのクラスはデフォルトで新しいクラスになるため、明示的にobject
継承する必要もなくなりました。
3-2. インスタンス
インスタンスは、単にオブジェクトと呼ぶこともありますが、クラスのコンストラクタとイニシャライザによって、属性に具体的な値が付与された実体のことを指します。
3-3. インスタンス化
インスタンス化は設計図であるクラスからインスタンスを生成する行為を指します。
3-4. インスタンス変数
インスタンス変数は、インスタンスごとに割り当てられた変数のことを指します。
3-5. クラス変数
クラス変数は、クラスとそのインスタンスが共有する変数のことを指します。
3-6. メソッド
メソッドはクラスまたはインスタンスに所属する関数のことを指します。
3-7. 静的メソッド
静的メソッドはインスタンス化しなくても、呼び出せるメソッドのことを指します。
3-8. クラスメソッド
クラスメソッドはクラスをオブジェクトとして操作するメソッドのことを指します。
3-9. メンバ
メンバはクラスまたはインスタンスの持つ名前空間に格納する要素です。名前空間には、通常メンバ変数(クラス変数またはインスタンス変数)とメンバ関数(各種メソッド)などが含まれます。
3-10. オーバーライド
オーバーライドは、子クラス(サブクラス・派生クラス)が親クラス(スーパークラス・基底クラス)から継承したメソッドを上書きする行為を指します。
3-11. カプセル化
カプセル化は、データと処理をオブジェクトとしてまとめて、境界線を作る行為を指します。
3-12. 継承
継承は、既存クラスの構造を受け継いだ子クラスを設計することを指します。is-aまたはhas-aの関係性を持たせるアーキテクチャです。
3-13. ポリモーフィズム
ポリモーフィズム(多態性)は、主にオーバーライドによって実現された子クラスの多様性を指します。例として以下のようなものが挙げられます。
class Animal:
def run(self):
print('Animal is running...')
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
def run_twice(animal):
animal.run()
animal.run()
run_twice(Animal())
run_twice(Dog())
run_twice(Cat())
実行結果:
Animal is running...
Animal is running...
Dog is running...
Dog is running...
Cat is running...
Cat is running...
つまり、あるクラスを入力とする処理は、その子クラスに対して何も修正する必要がなく、正常に動作できるという「リスコフの置換原則」による性質です。
3-14. 演算子オーバーロード
演算子オーバーロードは演算子の機能をユーザーが定義する行為を指します。Pythonでは全てのクラスはobject
クラスの子クラスで、それぞれの演算子オーバーロードは特殊メソッドにより実現されているので、性質としてはポリモーフィズムの1種になります。演算子オーバーロードに関する特殊メソッドは以下のようになります。
class MyNum:
def __init__(self,x):
self.__x = x
def __lt__(self, other):
print("__lt__")
return self.__x < other
def __le__(self, other):
print("__le__")
return self.__x <= other
def __eq__(self, other):
print("__eq__")
return self.__x == other
def __ne__(self, other):
print("__ne__")
return self.__x != other
def __gt__(self, other):
print("__gt__")
return self.__x > other
def __ge__(self, other):
print("__ge__")
return self.__x >= other
x = MyNum(100)
x < 10
x <= 10
x == 10
x != 10
x > 10
x >= 10
実行結果:
__lt__
__le__
__eq__
__ne__
__gt__
__ge__
上記は、演算処理にprint
処理を追加したものです。PythonにはNumpy
という数値計算ライブラリがあります。そして、a * b
という形で行列のアダマール積の計算ができるのはPythonが演算子オーバーロードをサポートしているからです。
3-15. 抽象化
抽象化は、カプセル化で、強い関連性のあるデータと処理だけをオブジェクトとしてまとめて、概念を形成することを指します。例えば、動物をAnimal
というクラスとして設計し、動物の状態を変数にし、動物の動作をメソッドにすることで抽象化できます。
3-16. ダック・タイピングとモンキーパッチ
この2つの概念はRubyコミュニティ由来のもので、動的言語の性質を表します。
モンキーパッチはランタイムでコードを拡張や変更する方法です。Pythonのオブジェクト指向プログラミングでは、クラスを動的に変更する場合に用語として使われます。
ダック・タイピングは動的型付けオブジェクト指向プログラミング言語の性質で、例えばrun_twice(animal)
というような関数を実行するとします。静的型付け言語は、引数の型を評価して、Animal
クラスまたはその派生でないと、実行自体が許されません。複数の型に対応させるために、ジェネリックスやオーバーロードなどの仕組みがが必要になります。それに対して、動的型付け言語は型の評価をせずに、run()
というメソッドを持ってれば正常に実行できます。「もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない」。
3-17. SOLID
SOLIDはオブジェクト指向プログラミングの分野において、ソフトウェア設計の5つの原則を記憶するための頭字語である。その5つの原則というのは、単一責任の原則、開放閉鎖の原則、リスコフの置換原則、インターフェース分離の原則と依存性逆転の原則です。
3-17-1. 単一責任の原則
単一責任の原則は1つのクラスに1つだけの責任を持たせるべきという原則です。「1つの責任」というのは少し曖昧ですので、実践では、あるクラスを変更する時の動機が2つ以上ある時は、単一責任と言えなくなります。例として、矩形を表すクラスRectangle
があるとして、GUIの描画機能と矩形の幾何学計算の2つのモジュールに使われています。ここのRectangle
クラスは単一責任の原則に違反しています。
3-17-2. 開放閉鎖の原則
開放閉鎖の原則は新しい要件に対して、コードを修正するのではなく、できるだけ拡張を行うべきという原則です。実践では、抽象化を用いてこの原則を実現することが多いです。Pythonのデコレーターは開放閉鎖の原則に則る機能で、既存メソッド、関数またはクラスを変更せずに新しい機能を実装できます。
3-17-3. リスコフの置換原則
リスコフの置換原則は親クラスが使われている箇所に、子クラスでも置換できるようにすべきという原則です。実践では、継承と多態性を用いてこの原則を実現しています。実例として、矩形を表すクラスRectangle
の子クラスとして、縦と幅が一致しないとエラーを起こすSquare
クラスがあります。そして、ある関数またはメソッドはRectangle
クラスを入力とし、内部で縦と幅に違い値を与えた場合、Square
クラスで置換できなくなるため、リスコフの置換原則に違反することになります。
3-17-4. インターフェース分離の原則
インターフェース分離の原則はクライアントに使わないメソッドへの依存関係を持たせるべきではないという原則です。言葉では理解しづらいが、下の例を見てください。
(出典:Agile Principles, Patterns, and Practices in C#)
この図はいくつかのクラスの関係を表しています。Door
クラスは、lock()
、un_lock()
やis_open()
のような扉と関連するメソッドを持っています。今度は、扉が一定時間開いていると、自動的に閉じるTimedDoor
を作ります。ここで、時間計測機能をTimerClient
というクラスに持たせ、Door
は直接TimerClient
を継承し、その機能を獲得します。そうすると、Door
を継承したTimedDoor
も時間計測機能を獲得できます。しかし、Door
は普通の扉で、時間計測機能は要らないので、インターフェース分離の原則に違反することになります。
解決策としては、以下のようなTimedDoor
の内部で、TimerClient
と接続するアダプターメソッドまたは変数を作成する方法とMixin継承の2種類の方法があります。
(出典:Agile Principles, Patterns, and Practices in C#)
3-17-5. 依存性逆転の原則
依存性逆転の原則は2つのルールを含みます。
- 上位モジュールは下位モジュールに依存してはならず、両方とも抽象に依存すべきです。
- 抽象は具体(実際の機能実現)に依存してはならず、具体は抽象に依存すべきです。
この原則はモジュール間のデカップリングのためのものです。例として以下のようなものがあります。
(出典:Agile Principles, Patterns, and Practices in C#)
ここの上位モジュールのPolicyLayer
は、下位モジュールのMechanismLayer
に依存し、下位モジュールのMechanismLayer
は実際の機能を実現するモジュールUtilityLayer
に依存しています。これは、依存性逆転の原則に違反するパターンです。
解決策として、以下のようなデザインができます。
(出典:Agile Principles, Patterns, and Practices in C#)
これで、PolicyLayer
は下位モジュールではなく、抽象インターフェースのPolicyServiceInterface
に依存するようになります。PolicyServiceInterface
と互換できるよう、MechanismLayer
は実装されます。
PolicyServiceInterface
が介在することで、PolicyLayer
とMechanismLayer
はお互い依存することなく、互換性を実現しました。MechanismServiceInterface
も同様です。抽象インターフェースは変更する可能性の低いもので、その介在によって各モジュールがデカップリングされます。
もう1つ例を挙げます。例えば、通常のPythonのWebアプリケーションでは、リクエスト→WSGIサーバー→WSGIアプリケーションという処理順序になります。ここのWSGIサーバーはGunicornやNginxのようなもので、WSGIアプリケーションはFlask、Djangoのようなものです。
GunicornはFlaskの実装を全く知らなくても、Flaskを呼び出すことが可能です。なぜなら、両方ともWSGIという抽象インタフェースに依存しているからです。ちなみに、ApacheはデフォルトではFlaskを呼び出すことはできません。しかし、mod_wsgiの拡張をインストールすればできるようになります。
WSGIの詳細の説明は割愛しますが、以下のコードを実行してみるのも良いでしょう。
from wsgiref.simple_server import make_server
def app(environ, start_response):
start_response('200 OK', [('Content-type', 'text/plain')])
return [b'Hello World']
if __name__ == "__main__":
with make_server('localhost', 5000, app) as httpd:
httpd.serve_forever()
3-18. GRASP
GRASPは、「General Responsibility Assignment Software Pattern」というオブジェクト指向システムの設計方針です。GRASPは、情報エキスパート、生成者、コントローラ、疎結合性、高凝集性、多態性、純粋人工物、間接化、変動から保護という9つのパターンを例として示しています。
3-18-1. 情報エキスパート
情報エキスパートパターンは、ある責務を果たすために必要な情報を全部持っているクラスがあるなら、そのクラスに任せるべきとしています。例えば、ECサイトのシステムにショッピングカートのShopCar
、商品のSKU
の2つのクラスあるとします。そして、「ショッピングカートに重複した商品を許容しない」という機能を実装します。同一商品がどうかを判断するための情報としてSKUID
はSKU
クラスの中にあるので、情報エキスパートパターンに従い、この機能はShopCar
クラスではなく、必要な情報を全部持っているSKU
クラスに実装するべきです。
3-18-2. 生成者
生成者パターンはクラスAとクラスBがあるとして、以下の条件で1つ以上満たした時、BにAを作成させるべきとしています。この場合、BはAの生成者になります。
- BがAを含みます。
- BがAを集約しています。集約というのはhas-aの関係性です。
- BがAの初期化情報を持っています。
- BがAのインスタンスを記録します。
- Bは頻繁にAを使用します。
例えば、ECサイトのシステムで、商品SKU
を管理する注文Order
というクラスがある場合、SKU
の作成は、Order
の内部で行うべきです。
3-18-3. コントローラ
コントローラパターンは、システムイベントをコントローラというオブジェクトに制御させるべきとしています。このコントローラはクラス、システム、またはサブシステムで、UIやインターフェースとインタラクトしないものにすべきです。
例えば、Ruby on Railsに使用されるMVCというアーキテクチャの「C」はコントローラの略です。
3-18-4. 疎結合性
結合性はシステムの各コンポーネント間の依存関係の強弱を表す尺度です。疎結合性パターンは各コンポーネント間の依存関係を弱くするようにシステムを設計すべきとしています。依存関係を弱くするために、import
などを最小限にしたり、クラスメンバのアクセス権限を厳しくしたり、クラスをイミュータブルオブジェクトにしたりするような手法があります。
ECサイトのシステムを例とすると、例えば、商品のトータル価格を計算する機能を追加する時、新しいクラスなどを作成して、SKU
をimport
して、その金額を集計するメソッドを作るより、すでにSKU
と依存関係を持っているOrder
に追加したほうが、不必要な依存関係を作ることがなくなります。
3-18-5. 高凝集性
凝集性はあるオブジェクト(モジュール)の持っている責務(機能)間の関連性の強弱を表す尺度です。高凝集性パターンはオブジェクトに適切に責務を集中すべきとしています。
またECサイトのシステムを例としますが、注文データのDAOクラスOrderDAO
を作成し、データ保存用のメソッドSaveOrder()
を実装します。Excelに保存する機能とDBに保存する機能を実現したい時は、まとめてOrderDAO
に実装するより、それぞれ、違うクラスを実装し、OrderDAO
を継承して、仮想メソッド(Pythonでは抽象メソッドとして実装されているため以降抽象メソッドと記載する)のSaveOrder()
をオーバーライドしたほうがいいが凝集性が高くなります。
3-18-6. 多態性
多態性は3-13. ポリモーフィズムで紹介した概念で、ここではそれをパターン化して、システム設計のルールとしています。多態性パターンはクラスの変動しがちな部分を抽象的なメソッドなどとして実装し、ポリモーフィズムを持たせて、その具体的な実現は子クラスで実装すべきとしています。
例えば、Shape
という抽象クラスを作り、Draw()
という描画用の抽象メソッドを実装します。Shape
を継承して、矩形Rectangle
、円Round
をそれぞれ作り、内部でDraw()
をオーバーライドし、各自の描画機能を実現するのは多態性パターンに則った設計になります。こうすることで、次に菱形Diamond
を追加したい時は、システム構造を変えずに同じやり方で作成できます。
3-18-7. 純粋人工物
システムを設計する時、高凝集性と疎結合性は矛盾します。高凝集性はクラスを細分化して、責務をそれぞれに集中させるようにするが、それぞれのクラスは協力し合わないと、正常に動作しないので、どうしても結合性を高くしてしまいます。
純粋人工物は人工的なクラス、すなわち抽象クラスを作成し、凝集性と結合性をバランスを調整します。例えば、図形の描画機能の例ですが、今度はWindowsとLinux両方対応する機能を追加します。それぞれのOSのシステムコールや構造自体は違うので、描画機能Draw()
も違う形で実装しなければなりません。ここで、抽象基底クラスのAbstractShape
を追加することで、凝集性を下げず、結合性もそれほど上げないままシステムを実現できます。
3-18-8. 間接化
間接化パターンは、2つのクラスの間に仲介としたオブジェクトを設けることで、クラス間の結合性の軽減を促進する設計方法です。MVCアーキテクチャーでは、Model
に直接View
とやりとりさせず、間にContoroller
を置くのは間接化パターンに則った設計です。3-17-4. インターフェース分離の原則で紹介した中間にあるインターフェース抽象クラスも同じ思想の設計です。
3-18-9. 変動から保護
変動から保護パターンは3-17-2. 開放閉鎖の原則と類似しています。変動から保護するために、不安定な部分を統一したインターフェースでカプセル化します。そして、変化が生じた場合はインターフェースを変更するのではなく、追加をします。古いコードを変えなくても機能を拡張できるのが目的です。3-17-2. 開放閉鎖の原則で例として出したPythonのデコレーターの他に、ORMは典型的な変動から保護パターンで、DBを変更してもクライアント側に影響を与えることはないです
3-19. デザインパターン
デザインパターンは、オブジェクト指向プログラミングにおいての設計ノウハウです。前述のSOLIDとGRASPのような設計方針(Design Principle)と違って、デザインパターンは過去の開発者が案出した経験則のようなものです。
4. Pythonのオブジェクト指向の基本
オブジェクト指向に関する概念を説明しました。これから、4つのクラスを作って、Pythonのオブジェクト指向プログラミングの基本構造について見てみます。
- Animal:各種クラス変数、メソッドについて
-
Dog:プロパティを定義する
property
について - Cat:プライベート変数とメソッドの継承・オーバーライドについて
-
Tiger:クラスメンバの継承用の
super
について
4-1. クラスの変数とメソッド
Pythonのクラスには変数とメソッドがあります。そして、それぞれ色々な種類があります。
- 変数はクラス変数とインスタンス変数があります。
- メソッドはいクラスメソッド、インスタンスメソッド、静的メソッドがあります。
下のコードで、各種変数とメソッドの定義について、コメントで説明します。
from types import MethodType
class Animal:
# ここはクラス変数を定義する場所
the_name = "animal" # クラス変数
def __init__(self, name, age): # イニシャライザ
self.name = name # インスタンス変数
self.age = age
# ここはメソッドを定義する場所
def sleep(self): # インスタンスメソッド
print("{} is sleeping".format(self.name))
def eat(self, food): # 引数付きのインスタンスメソッド
print("{} is eating {}".format(self.name, food))
@classmethod
def speak(cls, adjective): # クラスメソッド
print("I am a {} {}".format(adjective, cls.the_name))
@staticmethod
def happening(person, do): # 静的メソッド
print("{} is {}ing".format(person, do))
def drink_water(self):
print("{} is drinking water".format(self.name))
検証:
adam = Animal(name="Adam", age=2) # インスタンス化
print('adam.the_name: {}'.format(adam.the_name)) # インスタンスからクラス変数を呼び出す
# 実行結果:adam.the_name: animal
print('Animal.the_name: {}'.format(Animal.the_name)) # クラスからクラス変数を呼び出す
# 実行結果:adam.name: Adam
print('adam.name: {}'.format(adam.name)) # インスタンス変数を呼び出す
# 実行結果:Animal.the_name: animal
adam.sleep() # インスタンスメソッドを呼び出す
# 実行結果:Adam is sleeping
adam.eat("meat") # 引数付きのインスタンスメソッドを呼び出す
# 実行結果:Adam is eating meat
adam.speak("happy") # インスタンスからクラスメソッドを呼び出す
# 実行結果:I am a happy animal
Animal.speak("sad") # クラスからクラスメソッドを呼び出す
# 実行結果:I am a sad animal
adam.happening("Tim", "play") # インスタンスから静的メソッドを呼び出す
# 実行結果:Tim is playing
Animal.happening("Mary", "watch") # クラスから静的メソッドを呼び出す
# 実行結果:Mary is watching
Animal.the_name = "Animal" # クラス変数を修正
print('adam.the_name: {}'.format(adam.the_name))
# 実行結果:adam.the_name: Animal
adam.the_name = "animal" # インスタンスから修正
print('Animal.the_name: {}'.format(Animal.the_name))
# 実行結果:Animal.the_name: Animal
adam.age = 3 # インスタンス変数を修正
# メソッドのバインディング(モンキーパッチ)
adam.drink_water = MethodType(drink_water, adam) # インスタンスにバインディングする
adam.drink_water()
# 実行結果:Adam is drinking water
print(adam.drink_water)
# 実行結果:<bound method drink_water of <__main__.Animal object at 0x7ffd68064310>>
try:
Animal.drink_water
except AttributeError as e:
print(e)
# 実行結果:type object 'Animal' has no attribute 'drink_water'
Animal.drink_water = MethodType(drink_water, Animal) # クラスにバインディングする
adam.drink_water()
# 実行結果:Adam is drinking water
Animal.drink_water = drink_water # 直接代入でメソッドをバインディングする
adam.drink_water()
# 実行結果:Adam is drinking water
- クラス変数はクラスが持つ変数で、クラスとインスタンス両方で使えます。
- インスタンス変数は各インスタンスに所属するもので、そのインスタンスのみ使用できます。
- インスタンスメソッドはインスタンスが使うメソッドで、
self
というインスタンス自身を指す引数を定義する必要があります。 - クラスメソッドはクラスとインスタンス両方が使えるメソッドで、
cls
というクラスを指す引数を定義する必要があります。 - 静的メソッドはクラス内部で管理する普通の関数で、クラスとインスタンス両方が使えます。
- クラスからクラス変数を修正すると、インスタンスから呼び出す時に変更されます。
- インスタンスからクラス変数を修正すると、他のクラスやインスタンスに影響を与えません。
- メソッドのモンキーパッチは
MethodType
か直接代入で実現できます。 - インスタンスにメソッドをバインディングすると、元のクラスや他のインスタンスはバインディングされたメソッドが使えません。クラスににバインディングすると、全てのインスタンス(バインディングする前に作成したインスタンスも含む)に伝播します。
4-2. プロパティ
Animal
を継承したDog
クラスを作成し、property
やそれに関連するデコレーターを見てみます。これらのデコレーターはメソッドをプロパティ(変数)に変換するもので、以下の2つのメリットがあります。
- インスタンス変数のように
()
なしで呼び出せます。 - 変数の評価機能などの動的な処理を追加でき、合法性を保証できます。
デコレーター以外に、property
関数で上記の処理を実現できる方法もあります。
from functools import cached_property
class Dog(Animal): # クラスの継承
def eating(self):
print("{} is eating".format(self.name))
@property
def running(self):
if self.age >= 3 and self.age < 130:
print("{} is running".format(self.name))
elif self.age > 0 and self.age < 3:
print("{} can't run".format(self.name))
else:
print("please input true age")
@property # プライベートな変数を取得する
def country(self):
return self._country
@country.setter # メソッド名.setter
def country(self, value): # プライベートな変数に値を代入する
self._country = value
@country.deleter # メソッド名.deleter
def country(self): # プライベートな変数に値を削除する
del self._country
print("The attr country is deleted")
# property関数で上記のデコレーターと同じ機能を実現
def get_city(self):
return self._city
def set_city(self, value):
self._city = value
def del_city(self, value):
del self._city
city = property(get_city, set_city, del_city, "I'm the 'city' property.")
@cached_property # キャッシュされるproperty
def official_name(self):
return 'Mr.{} - the Dog'.format(self.name)
検証:
david = Dog("David", 2)
david.eating()
# 実行結果:David is eating
david.running # ()なしで呼び出す
# 実行結果:David can't run
dean = Dog("Dean", 4)
dean.running
# 実行結果:Dean is running
# デコレーターによる方法
david.country = "America"
print(david.country)
# 実行結果:America
del david.country
# 実行結果:The attr country is deleted
# property関数による方法
david.city = "NewYork"
print(david.city)
# 実行結果:NewYork
# キャッシュされるproperty
print(david.official_name)
# 実行結果:Mr.David - the Dog
-
@property
デコレーターはメソッドを変数に変換します。 -
property
関数でも同じ処理を実現できます。4番目の引数"I'm the 'city' property."
という文字列はドキュメントで、Dog.city.__doc__
で確認できます。 -
@cached_property
はPython 3.8で実装された値がキャッシュされるproperty
です。計算量の高い変数処理をする時、キャッシュされると再計算が必要なくなるので性能向上に繋がります。
4-3. プライベート変数とメソッドの継承・オーバーライド
Cat
クラスとその子クラスBlackCat
を定義し、プライベート変数とメソッドの継承・オーバーライドについて見ていきます。
- プライベートな変数は外部から使うことが制限される変数です。
- 子クラスは親クラスを継承する時、親クラスのメソッドを全部継承しますが、子クラスの中で同じ名前のメソッドを定義すると、継承されたメソッドがオーバーライドされます。イニシャライザメソッドの
__init__
も同様です。
class Cat(Animal):
def __init__(self, weight): # 親クラスの__init__をオーバーライド
self.__weight = weight
self._weight = weight + 1
self.weight = self._weight + 1
def get_weight(self):
print("My _weight is {}kg".format(self._weight))
def get_real_weight(self):
print("Actually my __weight is {}kg".format(self.__weight))
class BlackCat(Cat):
def get_weight(self): # 親クラスのメソッドをオーバーライド
print("My weight is {}kg".format(self.weight))
def get_real_weight(self):
print("Actually my _weight is {}kg".format(self._weight))
def get_actual_weight(self):
print("My __weight is exactly {}kg".format(self.__weight))
検証:
cole = Cat(5)
print("Cole's weight: {}kg".format(cole.weight))
# 実行結果:Cole's weight: 7kg
# _xは外部からの利用を推奨しないプライベート変数で、利用すること自体は制限されない
print("Cole's _weight: {}kg".format(cole._weight))
# 実行結果:Cole's _weight: 6kg
# __xは外部からの利用をを禁止するプライベート変数で、利用することは制限され、_<class>__xの形で強制的に呼び出せる
print("Cole's __weight: {}kg".format(cole._Cat__weight))
# 実行結果:Cole's __weight: 5kg
cole.get_real_weight() # メソッドで内部から__xを利用できる
# 実行結果:Actually my __weight is 5kg
cain = BlackCat(5)
cain.get_weight()
# 実行結果:My weight is 7kg
# _xは制限されないため、子クラスからでも呼び出せる
cain.get_real_weight()
# 実行結果:Actually my _weight is 6kg
# 親クラスのプライベート変数の__xを子クラスの内部から素直な方法では利用できない
try:
cain.get_actual_weight()
except AttributeError as e:
print(e)
# 実行結果:'Blackcat' object has no attribute '_Blackcat__weight'
-
weight
は普通の変数で、外部から利用できます。 -
_weight
のような1つのアンダースコアが付いてる変数は外部からの利用を推奨しないプライベート変数で、利用すること自体は制限されません。ただし、オブジェクト名(クラス名、関数名、モジュールスコープの変数名など)にする場合、from module import *
ではimportされません。 -
__weight
のような2つのアンダースコアが付いてる変数は外部からの利用を禁止するプライベート変数です。ただし、<class>._<class>__x
の形で強制的に呼び出せます。継承による属性の衝突を避けたい場合に使用するべきです。 - 変数名のパターンによる違う動作の実現は「名前修飾(Name Mangling)」と言います。
- 子クラスの中で親クラスが持っているメソッドと同じ名前のメソッドを定義すると、オーバーライドすることができます。
4-4. クラスメンバの継承
Tiger
とWhiteTiger
を定義し、super
の使い方について見ていきます。super
は子クラスの中で親クラスの変数やメソッドを呼び出すための関数です。
class Tiger(Animal):
def speak(self):
return "I'm a tiger not Lulu's song"
def eat(self):
return "{} is eating".format(self.name)
class WhiteTiger(Tiger):
def __init__(self, name, age, height):
super().__init__(name, age)
self.height = height
def speak(self):
return super().speak().replace("tiger", 'white tiger')
def eat(self):
return super().eat()
検証:
tony = WhiteTiger("Tony", 10, 100)
print(tony.eat())
# 実行結果:Tony is eating
print(tony.speak())
# 実行結果:I'm a white tiger not Lulu's song
-
return super().eat()
は親クラスのeat
メソッドを返しているだけで、子クラスの中でeat
メソッドを定義しなければsuper
を使う必要がありません。 -
super().__init__(name, age)
は、親クラスのイニシャライザ__init__
を実行します。これがないと、self.name
とself.age
を呼び出せません。super().__init__(name, age)
と同等な書き方は以下のようにいくつかあります。
1. 親クラスの変数を再定義します。
def __init__(self, name, age, height):
self.name = name
self.age = age
self.height = height
2. 親クラスの__init__
を明示的に呼び出します。親クラスの名前を変えると、呼び出された箇所を全部修正しなければなりません。
def __init__(self, name, age, height):
Tiger.__init__(self, name, age)
self.height = height
Discussion