Pythonでざっくり理解するオブジェクト指向
はじめに
初心者プログラマはだいたいは同じところでつまずきます.
それは「クラス」.
もっと大域的に言えばオブジェクト指向です.
この「オブジェクト指向」という考え方は,プログラマ初学者にとって実態がわからず大きな壁になりがちです.
また,このワードで検索しても「何言ってんコイツ」となってしまう人も少なくないはずです.
というか,中級者レベルであっても混乱しがちです.
そこで,この記事ではPythonを通してオブジェクト指向とは何かを "ざっくり" と理解できることを目的としています.
僕の想定としては,
- この記事でオブジェクト指向のアウトラインや,ざっくりとしたイメージを掴む
- 他の人が書いた記事で,より深く理解する (できればPython以外の言語で)
といったステップになっています.
なので,この記事だけでオブジェクト指向のすべてを理解できるわけではありません.
あくまで,オブジェクト指向の主要概念であるクラスを主軸とした内容ですので悪しからず...
そしてなぜPythonを使うかというと,いくつか理由があります.
- Python人口が多い
- Pythonはコードが読みやすく,他言語で学んでいる人にも理解しやすい
- 筆者の好み
です.
まあ,前置きはさておき記事を進めます.
オブジェクト指向(ざっくり)
早速ですが,オブジェクト指向についてざっくり説明します.
オブジェクト指向とは,その名の通り"モノ"として扱える様にプログラミングをする"考え方"です.
なんのこっちゃと思うかもしれませんが,身の回りのものを想像してみてください.
例えばスマートフォン.
スマートフォンは画面をタッチすればアプリを起動できたり,電話をかけることができたり便利ですね.
でも,利用者である僕らはその内部がどんな処理をしているか知らずに使えています.
これが,"モノ"としてプログラムされているということです.
きっと実際は,タッチされたら複雑な処理が施されているでしょう.
しかし,どんなに複雑な処理がプログラムされていようとも,ユーザは深く考えることなく扱うことができる.
これがオブジェクト指向の大事な考え方です.
そしてそのモノを作るのに,クラスという概念が主軸になってきます.
クラスを使ってプログラムを書けば,それはオブジェクト指向プログラミングとほとんど言えるでしょう.
とまあ,言葉で書かれてもピンとこないかもしれません.
オブジェクト指向ではよく三大要素として以下が挙げられます.
- カプセル化
- 継承
- ポリモーフィズム (多様性)
これらは,それぞれ超大事な要素ですが,この記事では一番大事なカプセル化に注目して解説を行います.
まあでも,一応最後に継承とポリモーフィズムに少しだけ触れます.
それでは実際に作りましょう.
今回作るモノ
この記事では,オブジェクト指向をざっくり学ぶために,「Robotクラス」を作ります.
いや,正直なんでも良いのですが,シンプルでわかりやすいものが良いかなと.
なんのロボットというわけでもなく,ただのロボットです.
Pythonで実装していく
それでは実装していきましょう.
各ステップごとに分けて解説します.
クラスファイルと実行ファイルを作る
本当は一つのファイルにまとめてしまってもOKなんですが,わかりやすくするために分けます.
- main.py
- robot.py
の二つを作成してください.
以上です.
クラスの外枠を作成する
Pythonに限らず,クラス設計には必須なものがいくつか存在します.
まずは,クラス定義.
そしてコンストラクタです.(Pythonなどではイニシャライザとも言うらしいですね)
1 2 3 4 5 6 7 8 9 10 11 | # robot.py class Robot: """ Robot クラス """ def __init__(self): """ コンストラクタ """ pass |
コンストラクタは,クラスからモノを新しく作るときに一番最初に呼ばれるメソッドです.
正直な話,コンストラクタは無くても良いですが,よほどのことがない限り作ったほうが可読性向上にもつながります.
そして,クラスはよく"設計図"と例えられますが,その認識でOKです.
設計図をもとにモノを作るということ,それがオブジェクト指向の第一歩です.
- メソッド:クラス内に定義する関数(メンバ関数とも)
- Docstring: """ で囲まれた特別なコメント.主に関数などの定義が記述される.
- pass :「何もしない」をする文.未定義関数などのエラー回避のために使われる.
実行ファイルから呼び出してみる
それでは今作ったクラスからモノを作ってみます.
1 2 3 4 5 | # main.py from robot import Robot # クラスをインポート if __name__ == '__main__': robot = Robot() # インスタンス生成 |
このとき,クラスから作成した変数(モノ)をインスタンスやオブジェクトと呼びます.
1 | $ python main.py |
もちろん,クラスには何も細かい設計はしていませんので実行しても何も起こりません.
- if __name__ == '__main__': :そのファイルを実行した時だけ呼ばれる,言わばメイン関数のようなもの.無くても良いが合ったほうが可読性はあがる.好みや慣習による.
メンバ変数を定義する
次にメンバ変数を定義してみます.
メンバ変数はプロパティなどとも言われますが,クラスの持つ要素を指します.
今回のRobotクラスであれば,大きさや名前,色など,その"モノ"自身が持つ特性などが一般に定義されます.
Pythonでは,一般的にコンストラクタ内で定義し初期化を行います.
このとき, self をつけて扱います.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # robot.py class Robot: """ Robot クラス """ def __init__(self, name, height, color): """ コンストラクタ :param name: 名前 :param height: 高さ :param color: 色 """ self.name = name self.height = height self.color = color |
このように self の有無で変数は別物として扱われます.
この場合,引数として各メンバ変数の初期値を受け取り,メンバ変数を定義するごく一般的な超普通のコンストラクタの使い方です.
そうしたらインスタンスの作り方も変えなければなりません.
1 2 3 4 5 6 7 8 9 | # main.py from robot import Robot # クラスをインポート if __name__ == '__main__': robot = Robot( name='Tanaka', height=300, color='orange' ) # インスタンス生成 |
name= などは順番が合っていれば省略可能ですが,可読性のためになるべく記述することが望ましいです.
これで,オレンジ色で300cmの"Tanaka"というロボットが生成されました.
このとき,インスタンスはクラスからたくさん生成できます.
これが,クラスが設計書と呼ばれる所以ですね.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # main.py from robot import Robot # クラスをインポート if __name__ == '__main__': robot1 = Robot( name='Tanaka', height=300, color='orange' ) # インスタンス生成 1 robot2 = Robot( name='Suzuki', height=1500, color='pink' ) # インスタンス生成 2 |
メソッドをもっと作る
まだこのロボットは何もできません.
なので何かできるようにしましょう.
今回は
- 自己紹介
- 色をランダムに変える
メソッドを作ってみます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | # robot.py import random class Robot: """ Robot クラス """ def __init__(self, name, height, color): """ コンストラクタ :param name: 名前 :param height: 高さ :param color: 色 """ self.name = name self.height = height self.color = color def print_status(self): """ 自己紹介する (デバッグ用の出力関数) :return: """ print('My name is {}'.format(self.name)) print('I am {}cm tall.'.format(self.height)) print('My body color is {}.\n'.format(self.color)) def change_color(self): """ ランダムに色を変える :return: """ self.color = random.choice(['red', 'blue', 'green', 'orange']) print('My body color is changed!\n') |
そうしたら,main内は以下の記述だけでOKです.
1 2 3 4 5 6 7 8 9 10 11 12 13 | # main.py from robot import Robot # クラスをインポート if __name__ == '__main__': robot = Robot( name='Tanaka', height=300, color='orange' ) # インスタンス生成 robot.print_status() robot.change_color() robot.print_status() |
1 2 3 4 5 6 7 8 9 10 | $ python main.py My name is Tanaka I am 300cm tall. My body color is orange. My body color is changed! My name is Tanaka I am 300cm tall. My body color is blue. |
このようにメソッドの定義をしてしまったら,どんなに長文コードを書いても, robot.print_status() のようにひとつ命令を書けば済むのです.
これが,オブジェクト指向(クラス)の最大の重要要素だと思います.
そしてこのように,クラスとしてデータやメソッドをひとまとまりにしてしまう考えをカプセル化と言います.
もう一度言いますが,カプセル化はオブジェクト指向の超重要要素です.
あと,関数名は基本 "動詞" にしましょうね.
do() とか do_something() という形が一般的です.
カプセル化の情報隠蔽の話
このカプセル化には本来,"情報隠蔽(じょうほういんぺい)"という概念があります.
これは,クラス内部に持つメンバ変数などは外から簡単に書き換えられる状態であってはいけない,というもの.
その言葉のごとく隠すことを指します.
例えば,スマホやテレビにもシリアル番号のような,変えられてはいけない内部パラメータがあるはずです.
それらは,ユーザから安易に変更されないようにするのが普通です.
この情報隠蔽はオブジェクト指向のカプセル化において,これまた重要な要素です.
JavaやC++といった言語では,隠すものと公開するものをそれぞれ,privateとpublicで定義します.
が,Pythonにおいて基本それらの区別は基本的にはありません.
ただ,"擬似的に"プライベートなメンバ変数を定義することは可能です.
先ほどのRobotクラスで,新たにシリアルID を定義してみましょう.
これは外部から書き換えられたら溜まったもんじゃありません.
Pythonでは以下のように,アンダーバーを2つ使用して定義します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | # robot.py import random import uuid from datetime import datetime class Robot: """ Robot クラス """ def __init__(self, name, height, color): """ コンストラクタ :param name: 名前 :param height: 高さ :param color: 色 """ self.name = name self.height = height self.color = color # 日付をもとにユニークなIDを生成 self.__id = uuid.uuid5(uuid.NAMESPACE_DNS, datetime.today().strftime('%Y%m%d%H%M%S')) |
今回は割としっかり目にユニークIDを生成してみました.
このように定義すると,以下の様なアクセスは不可能になります.
1 2 3 4 5 6 7 8 9 10 11 12 | # main.py from robot import Robot # クラスをインポート if __name__ == '__main__': robot = Robot( name='Tanaka', height=300, color='orange' ) # インスタンス生成 print(robot.name) # できる print(robot.__id) # できない |
1 2 3 4 5 6 | $ python main.py Tanaka Traceback (most recent call last): File "/Users/araki/PycharmProjects/samples/main.py", line 12, in <module> print(robot.__id) # できない AttributeError: 'Robot' object has no attribute '__id' |
じゃあ,変更は出来ないのは良いけど,本当にそのデータを参照したいときは?
そう言う時は,ゲッター(Getter)というメソッドを定義することがあります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | # robot.py import random import uuid from datetime import datetime class Robot: """ Robot クラス """ def __init__(self, name, height, color): """ コンストラクタ :param name: 名前 :param height: 高さ :param color: 色 """ self.name = name self.height = height self.color = color # 日付をもとにユニークなIDを生成 self.__id = uuid.uuid5(uuid.NAMESPACE_DNS, datetime.today().strftime('%Y%m%d%H%M%S')) def print_status(self): pass def change_color(self): pass def get_id(self): return self.__id |
ただ返すだけのメソッドです.
1 2 3 4 5 6 7 8 9 10 11 | # main.py from robot import Robot # クラスをインポート if __name__ == '__main__': robot = Robot( name='Tanaka', height=300, color='orange' ) # インスタンス生成 print(robot.get_id()) # できる |
1 2 | $ python main.py f53496fc-4678-5f53-9173-174bf4f0c291 |
こうすることで,変更は許されないけど取得は許される,いわばプライベートなメンバ変数が定義できます.
しかし,さっき"擬似的に"隠蔽が可能と言いました.
実はこれ,以下の様に書けばアクセスが可能なのです.
1 2 3 4 5 6 7 8 9 10 11 12 | # main.py from robot import Robot # クラスをインポート if __name__ == '__main__': robot = Robot( name='Tanaka', height=300, color='orange' ) # インスタンス生成 print(robot.get_id()) # できる print(robot._Robot__id) # Getterを通さずともできちゃう |
インスタンス._クラス__プライベートメンバ変数 という形ですね.
これは,暗黙の了解として「やってはいけない」とはなっていますが,詰まるところPythonでは完全な情報隠蔽はできません.
というかPythonでは実際このようにして情報を隠すことはあまりしません.
Pythonは紳士の言語なので暗黙の了解でどうにかなるのです.(?)
ですが,JavaやC++ではメンバ変数は基本privateです.
ですので,Pythonを使っていても頭の片隅には,「メンバ変数は基本隠蔽するもんなんだな」と覚えておいてください.
ぶっちゃけオブジェクト指向を完璧に学びたければPythonじゃなくて他の言語のほうが良くね?
ここまでが,オブジェクト指向の一番大事な要素カプセル化の話でした.
まとめると
- カプセル化はオブジェクト指向において一番大事な要素
- クラス設計≒カプセル化
- 書き換えがされないようにするには情報を隠蔽する
- Pythonは例外で他のJavaやC++では,メンバ変数は基本Private
継承とポリモーフィズムの話
最後に継承とポリモーフィズムについて軽く触れます.
さっきみたいに詳細には解説しません.
継承
継承は,作ったクラス(親クラス)をもとに新しいクラス(小クラス)を生成することを指します.
クラスは集合のようなもので,今僕らが作ったものはRobotクラスであり,Robotという名前の集合を作りました.
それを継承して,Gundamクラスを作れば引き継げるものが多く,クラス設計の手間を省くことができます.
これが継承の考え方です.
Pythonでもクラスの継承が可能です.
1 2 3 4 5 6 7 8 9 10 | class Gundam(Robot): """ Gundamクラス 筆者はガンダム詳しくない """ def __init__(self, name, height, color): super().__init__(name, height, color) # 加えてGundamクラス特有のコンストラクタを定義可能 pass |
もちろんこのGundamクラスを親にして,また小クラスも生成可能です.
大事なのは,「小クラス is a 親クラス」の関係です.
反対は成り立ちません.
Gundam is a Robot なのであって,Robot is a Gundamではありません.
あと,ちなみに小クラスから親クラスのプライベート変数にはアクセス不可です.
参考までに,小クラスからもアクセスしたければ,JavaやC++ではかわりにprotectedという指定子をつけます.
やはりPythonではメンバ変数は基本publicなもので良いでしょう.
ポリモーフィズム
ポリモーフィズムは日本語では多様性などと訳されます.
ポリモーフィズムとは抽象化であり,重要なのはいかに親クラスから多様性を持って継承できるかです.
継承とポリモーフィズムは密接な関係にあります.
今回Robotクラスを作りましたが,とても抽象的なクラスです.
例えば,Robotクラスに walk() メソッドを作ったとしましょう.
ですがロボットといえど,ドラえもんのように二足歩行かもしれませんし,ルンバのように車輪型かもしれません.
これでは,このRobotクラスからRoombaクラスは作れません.(実際作れるけど)
なので, walk() ではなく move() のような抽象的な名前のメソッドにしましょう,ということです.
抽象化することで多様性につながります.
正直な話,一人で開発している分にはあまり縁がないかもしれません.
ですので,一人で開発しているのならば,とりあえずカプセル化を身に付けるのが良いでしょう!
おわりに
長くなってしましましたが,Pythonでざっくり理解するオブジェクト指向でした.
オブジェクト指向がわかると,プログラミングが楽しくなります.
他人が書いたコードをすぐに理解できる様になります.
なので,これを機に手を動かしながらしっかりとオブジェクト指向を理解してください.
そうそう,他人の書いたコードを読むことも良い勉強になるのでおすすめです.