👨‍💻

Pythonのオブジェクト指向プログラミングを完全理解 (1)

2023/02/13に公開

オブジェクト指向

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というオブジェクトのnameBobで、johnnameJohnなので、同じclassから作成されたインスタンスにもかかわらず、違うオブジェクトになり、同じメソッドを実行しても結果が異なります。

また、違うclassmoveメソッドは、違う結果を出力しています。例えば、BirdmoveThe bird named...を出力し、DogThe 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_birdmove_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. インターフェース分離の原則

インターフェース分離の原則はクライアントに使わないメソッドへの依存関係を持たせるべきではないという原則です。言葉では理解しづらいが、下の例を見てください。
Screen Shot 2020-10-17 at 23.31.01.png
(出典:Agile Principles, Patterns, and Practices in C#

この図はいくつかのクラスの関係を表しています。Doorクラスは、lock()un_lock()is_open()のような扉と関連するメソッドを持っています。今度は、扉が一定時間開いていると、自動的に閉じるTimedDoorを作ります。ここで、時間計測機能をTimerClientというクラスに持たせ、Doorは直接TimerClientを継承し、その機能を獲得します。そうすると、Doorを継承したTimedDoorも時間計測機能を獲得できます。しかし、Doorは普通の扉で、時間計測機能は要らないので、インターフェース分離の原則に違反することになります。

解決策としては、以下のようなTimedDoorの内部で、TimerClientと接続するアダプターメソッドまたは変数を作成する方法とMixin継承の2種類の方法があります。
Screen Shot 2020-10-17 at 23.46.32.png
Screen Shot 2020-10-17 at 23.46.55.png
(出典:Agile Principles, Patterns, and Practices in C#

3-17-5. 依存性逆転の原則

依存性逆転の原則は2つのルールを含みます。

  • 上位モジュールは下位モジュールに依存してはならず、両方とも抽象に依存すべきです。
  • 抽象は具体(実際の機能実現)に依存してはならず、具体は抽象に依存すべきです。

この原則はモジュール間のデカップリングのためのものです。例として以下のようなものがあります。
Screen Shot 2020-10-17 at 23.57.12.png
(出典:Agile Principles, Patterns, and Practices in C#

ここの上位モジュールのPolicyLayerは、下位モジュールのMechanismLayerに依存し、下位モジュールのMechanismLayerは実際の機能を実現するモジュールUtilityLayerに依存しています。これは、依存性逆転の原則に違反するパターンです。

解決策として、以下のようなデザインができます。
Screen Shot 2020-10-18 at 0.00.17.png
(出典:Agile Principles, Patterns, and Practices in C#

これで、PolicyLayerは下位モジュールではなく、抽象インターフェースのPolicyServiceInterfaceに依存するようになります。PolicyServiceInterfaceと互換できるよう、MechanismLayerは実装されます。

PolicyServiceInterfaceが介在することで、PolicyLayerMechanismLayerはお互い依存することなく、互換性を実現しました。MechanismServiceInterfaceも同様です。抽象インターフェースは変更する可能性の低いもので、その介在によって各モジュールがデカップリングされます。

もう1つ例を挙げます。例えば、通常のPythonのWebアプリケーションでは、リクエスト→WSGIサーバー→WSGIアプリケーションという処理順序になります。ここのWSGIサーバーはGunicornNginxのようなもので、WSGIアプリケーションはFlaskDjangoのようなものです。

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つのクラスあるとします。そして、「ショッピングカートに重複した商品を許容しない」という機能を実装します。同一商品がどうかを判断するための情報としてSKUIDSKUクラスの中にあるので、情報エキスパートパターンに従い、この機能はShopCarクラスではなく、必要な情報を全部持っているSKUクラスに実装するべきです。
Screen Shot 2020-10-18 at 21.30.32.png

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の内部で行うべきです。
Screen Shot 2020-10-18 at 21.32.06.png

3-18-3. コントローラ

コントローラパターンは、システムイベントをコントローラというオブジェクトに制御させるべきとしています。このコントローラはクラス、システム、またはサブシステムで、UIやインターフェースとインタラクトしないものにすべきです。

例えば、Ruby on Railsに使用されるMVCというアーキテクチャの「C」はコントローラの略です。

3-18-4. 疎結合性

結合性はシステムの各コンポーネント間の依存関係の強弱を表す尺度です。疎結合性パターンは各コンポーネント間の依存関係を弱くするようにシステムを設計すべきとしています。依存関係を弱くするために、importなどを最小限にしたり、クラスメンバのアクセス権限を厳しくしたり、クラスをイミュータブルオブジェクトにしたりするような手法があります。

ECサイトのシステムを例とすると、例えば、商品のトータル価格を計算する機能を追加する時、新しいクラスなどを作成して、SKUimportして、その金額を集計するメソッドを作るより、すでにSKUと依存関係を持っているOrderに追加したほうが、不必要な依存関係を作ることがなくなります。
Screen Shot 2020-10-18 at 21.33.12.png

3-18-5. 高凝集性

凝集性はあるオブジェクト(モジュール)の持っている責務(機能)間の関連性の強弱を表す尺度です。高凝集性パターンはオブジェクトに適切に責務を集中すべきとしています。

またECサイトのシステムを例としますが、注文データのDAOクラスOrderDAOを作成し、データ保存用のメソッドSaveOrder()を実装します。Excelに保存する機能とDBに保存する機能を実現したい時は、まとめてOrderDAOに実装するより、それぞれ、違うクラスを実装し、OrderDAOを継承して、仮想メソッド(Pythonでは抽象メソッドとして実装されているため以降抽象メソッドと記載する)のSaveOrder()をオーバーライドしたほうがいいが凝集性が高くなります。
Screen Shot 2020-10-18 at 21.33.42.png

3-18-6. 多態性

多態性は3-13. ポリモーフィズムで紹介した概念で、ここではそれをパターン化して、システム設計のルールとしています。多態性パターンはクラスの変動しがちな部分を抽象的なメソッドなどとして実装し、ポリモーフィズムを持たせて、その具体的な実現は子クラスで実装すべきとしています。

例えば、Shapeという抽象クラスを作り、Draw()という描画用の抽象メソッドを実装します。Shapeを継承して、矩形Rectangle、円Roundをそれぞれ作り、内部でDraw()をオーバーライドし、各自の描画機能を実現するのは多態性パターンに則った設計になります。こうすることで、次に菱形Diamondを追加したい時は、システム構造を変えずに同じやり方で作成できます。
Screen Shot 2020-10-18 at 21.44.21.png

3-18-7. 純粋人工物

システムを設計する時、高凝集性と疎結合性は矛盾します。高凝集性はクラスを細分化して、責務をそれぞれに集中させるようにするが、それぞれのクラスは協力し合わないと、正常に動作しないので、どうしても結合性を高くしてしまいます。

純粋人工物は人工的なクラス、すなわち抽象クラスを作成し、凝集性と結合性をバランスを調整します。例えば、図形の描画機能の例ですが、今度はWindowsとLinux両方対応する機能を追加します。それぞれのOSのシステムコールや構造自体は違うので、描画機能Draw()も違う形で実装しなければなりません。ここで、抽象基底クラスのAbstractShapeを追加することで、凝集性を下げず、結合性もそれほど上げないままシステムを実現できます。
Screen Shot 2020-10-18 at 22.07.19.png

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. デザインパターン

デザインパターンは、オブジェクト指向プログラミングにおいての設計ノウハウです。前述のSOLIDGRASPのような設計方針(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. クラスメンバの継承

TigerWhiteTigerを定義し、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.nameself.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

続きはPythonのオブジェクト指向プログラミングを完全理解 (2)

Discussion