一流のプログラマーとして活躍するには、オブジェクト指向プログラミングの知識やスキルは、今や欠かせないものとなってきています。そして、Python はオブジェクト指向プログラミングに非常に適したプログラミング言語です。
そのため、Python の学習者にとって、オブジェクト指向プログラミングについて、正しく理解し、使いこなせるようになることは、非常に重要なことと言えます。
このページで、例を交えながら、一から解説していきますので、ぜひ参考にして頂ければ幸いです。
※コメント欄に頂いたご指摘について
当記事の内容が、『独学プログラマー Python言語の基本から仕事のやり方まで』のコピーであるとご指摘を頂きました。確かに、その通りで、当記事は当該書籍を丸々参考にして書きました。当記事の内容をわかりやすいと感じられたら、それは当該書籍の功績です。
当該書籍では、当記事で省いてしまったこともあり、当記事以上にわかりやすく、また初心者プログラマーに必要なことが、著者の実体験を根拠として、ありありと書かかれています。ぜひ、当記事よりも、当該書籍をお読み頂ければと思います。
『独学プログラマー Python言語の基本から仕事のやり方まで』
1. プログラミング・パラダイム
それでは、オブジェクト指向プログラミングについて始める前に、プログラミング・パラダイムについて解説しておきたいと思います。
プログラミング・パラダイムとは、プログラミングのスタイル全体を指す言葉です。大きく分けて、以下の 3 つのスタイルがあります。
- 手続き型プログラミング
- 関数型プログラミング
- オブジェクト指向プログラミング
これらの 3 つのスタイルの中で、プログラマーとしてやっていくには、関数型プログラミングかオブジェクト指向プログラミングの、どちらかを学ぶ必要があると言えます。
それぞれのスタイルの最も大きな違いの 1 つは「状態(ステート)」の持ち方にあります。
状態とは、プログラムが動いている時の変数のことです。例えば、グローバルステートは「グローバル変数という状態」の変数です。一方で、ローカルステートは、 def 文のブロック内に書かれている「ローカル変数という状態」です。
こうしたことは、「 Pythonのスコープ(グローバル変数とローカル変数)のルール」でも触れているのでご確認ください。そちらの記事で、グローバル変数に頼り過ぎると、予期せぬエラーに悩まされることになるという点も意識して頂ければと思います。
それでは、この「状態(ステート)」に着目しながら、それぞれのプログラミング・スタイルを上から順番に見ていきましょう。
2. 手続き型プログラミング
手続き型プログラミングは、「まずこれをやって、次にあれをやって、そしてそれをやって」というように、ボトムアップで一つずつ作り上げていくスタイルです。
例えば、以下のコードをご覧ください。
'''手続き型プログラミング的な書き方'''
x = 2 #まず x を定義して
y = 4 #次に y を定義して
z = 8 #そして z を定義して
xyz = x + y + z #最後に xyz の値を定義しています。
このコードでは、 1 行ごとにステートを変えて、最後に xyz の値を定義しています。手続き型プログラミングの場合、全てのデータをグローバル変数として持ちます。そして、データの操作は関数で行います。
例えば、次のコードは手続き型プログラミングです。
pop =[]
jpop =[]
def collect_songs():
song = "曲名を入力してください。"
ask ="p か j のどちらかを入力してください。 q で終了。"
while True:
gen = input(ask)
if gen == "q":
break
if gen =="p":
p = input(song)
pop.append(p)
elif gen == "j":
j = input(song)
jpop.append(j)
else:
print("不正な値です。")
print("pop songs:", pop)
print("jpop songs:", jpop)
collect_songs()
手続き型プログラミングは、これぐらいの小さなプログラムを書くには適しています。しかし、状態(ステート)を、全てグローバル変数に持たせているため、グローバル変数に頼り過ぎたプログラムになってしまいます。
そして、この書き方では、長く複雑なプログラムになればなるほど、プログラム内のあちこちの関数で、同じグローバル変数を使うようになります。
そのため、例えば、ある関数でグローバル変数を変更しているのに、また別の関数で、意図せず、グローバル変数を上書きしてしまうということが起きてしまいます。
そうなると、新しいコードが、それまでに書き上げたコードのデータを壊してしまい、予期せぬエラーに悩まされることになってしまうのです。最終的に、プログラムは、あっという間に修復不可能になってしまいます。
手続き型プログラミングにはこうした問題があるため、その解決策として、関数型プログラミングや、オブジェクト指向プログラミングが登場することになりました。
なお上のコードは、グローバル変数を「これでもか」というぐらい不要に使って書いています。ただし、手続き型プログラミングとはいえ、グローバル変数の多用を控える意識は、理解しやすくエラーの少ないコードを書くために重要です。
3. 関数型プログラミング
次に、関数型プログラミングを見てみましょう。これは、手続き型プログラミングからグローバルステートを排除したものです。
これによって、関数型プログラミングで書く関数は、グローバルステートに依存せず、その動作は、関数に渡された引数によってのみ変わるようになります。そして、関数の戻り値は、通常、他の関数に引数として渡されます。
例として、2 つの関数を見比べてみましょう。まずは 1 つ目です。
a = 0 #グローバル変数
'''関数内でグローバル変数 a を使っている。'''
def increment():
global a
a += 1
return a
'''実行してみます。'''
increment()
'''実行するたびにグローバル変数 a の値が変更されます。'''
print(a)
このコードは、関数の戻り値は、関数ブロック外のデータであるグローバル変数に依存しています。そして、この increment 関数を使うと、グローバル変数の a の値も変更されます。
それでは、次の関数をご覧ください。
'''この書き方は値を引数で渡しており、 a の値はグローバル変数に左右されません。'''
def increment(a):
return a + 1
'''実行してみます。'''
increment(0)
こちらの関数では、関数ブロックの外にある変数を参照していません。引数に値を送り、その引数に対して処理を行うというものになっています。そのため、予期せずグローバル変数が変更されてしまうというような不具合は起こりません。
このように、関数型プログラミングでは、そもそもグローバルステートが存在しないので、関数外のデータに依存するようなコードは不可能となり、関数外のデータが予期せず変更されてしまうというような副作用が起きないのです。
ただし、グローバルステートを持たせた方が、遥かに効率的なことも多々あるのですが、関数型プログラミングは、それすら排除してしまうという欠点があります。
4. オブジェクト指向プログラミング
オブジェクト指向プログラミングも、関数型プログラミングのように、グローバルステートを排除しているということは同じです。ただし、それを、オブジェクトのグループ毎に、個別の変数を設定することで実現します。
これから詳しく解説しますが、オブジェクト指向プログラミングは、コードの再利用を促進し、システムの開発や保守にかかるトータル時間を短縮できるという利点があります。ただし、プログラムを書く前の計画や設計が、非常に重要になります。
具体的に見ていきましょう。
例えば、バッグの中に、リンゴがいくつも入っているとします。
1 つ 1 つのリンゴが、それぞれオブジェクトです。それぞれのリンゴには、糖度や重さ、色といった共通の「属性」があります。そこで、オブジェクト指向プログラミングでは、リンゴクラスというものを作って、1 つ 1 つのリンゴが共通して持っている属性を、インスタンス変数として定義します。
ただし、糖度がどれぐらいか、重さが何グラムかというような「値」は、それぞれ異なりますね。そこで、リンゴというクラスから、1 つのリンゴ(インスタンス)を作る時に、値を与えます。
例えば、 Apple というクラスがあるとします。そして Apple クラスには、重さと色という共通属性があるとします。そこで、この共通属性を、weight と color という変数にします。
以下をご覧ください。なお、書式やコードの解説については、「Pythonのクラス(class)の使い方まとめ」で行なっていますので、ご確認ください。
class Apple:
def __init__(self, w, c):
self.weight = w
self.color = c
これで、Apple クラスのオブジェクトには、重さ (self.weight) と、色 (self.color) という変数ができました。
オブジェクト指向プログラミングでは、全てのオブジェクトを、このクラスから作ります。クラスから作ったオブジェクトのことを「インスタンス」と言います。そして、インスタンスを作る時に、あらかじめクラスで設定した変数に、値を渡します。
次のように書きます。
apple1 = Apple(10, "dark red")
これで、 weight は 10 で、color は “dark red” という変数と値を持つapple1というインスタンスができました。
このように、オブジェクト指向プログラミングでは、クラスに指定の変数を持たせて、インスタンスを作る時に、それに値を渡します。グローバル変数は一切使わずに、クラスで変数を定義するので、グローバルステートの問題に悩まされることがないのですね。
インスタンスの変数の値を変更するには、次のように書きます。
apple1.weight = 20 #値を変更しています。
apple1.color = "light red" #値を変更しています。
print(apple1.weight)
print(apple1.color)
最初、インスタンス apple1 の変数の値は weight: 10 , color: “dark red” でしたね。それを、それぞれ 20 と “light red” に変更しています。
クラスからは、いくつでもインスタンスを作ることができます。
class Apple:
def __init__(self, w, c):
self.weight = w
self.color = c
print("Created.")
apple2 = Apple(6, "light red")
apple3 = Apple(9, "dark red")
apple4 = Apple(12, "yellow")
また、クラスには、変数だけではなく、機能(メソッド)も設定することができます。
以下のコードは、長方形を表すクラスに、複数のメソッドを定義しています。 area メソッドは面積を計算し、change_size メソッドは長方形のサイズを変更します。
class Rectangle:
#Rectangle クラスの共通属性(インスタンス変数)
def __init__(self, w, h):
self.width = w
self.height = h
#Rectangle クラスの面積計算メソッド area()
def area(self):
return self.width*self.height
#Rectangle クラスのサイズの変換メソッド change_size()
def change_size(self, w, h):
self.width = w
self.height = h
rect1 = Rectangle(10, 20) #インスタンスを作ります。
print(rect1.area()) #メソッドを使って面積を計算します。
rect1.change_size(15, 25) #メソッドを使ってサイズを変更します。
print(rect1.area()) #メソッドを使って面積を計算します。
このように、オブジェクト指向プログラミングでは、クラスに固有の変数を持たせて、そのクラスから、オブジェクト(インスタンス)を作ります。
一度作ったインスタンスの値は、アクセス権を設定することで、変更不可能にすることも自由です。さらに、クラスに固有のメソッドを作ることもできます。そのため、プログラミングにおける全ての処理が、クラスの中で完結します。
これによって、グローバル変数の問題に悩まされることはなくなります。また、それだけでなく、最終的には、開発時間の短縮や、保守の簡素化に繋がります。
5. オブジェクト指向プログラミングの 4 大要素
ここから、さらに詳しくオブジェクト指向プログラミングについて、見ていきまさよう。オブジェクト指向プログラミングには、以下の 4 つの主要な概念があります。
- カプセル化
- 抽象化
- ポリモーフィズム
- 継承
Python は、Java や Ruby と同じように、これらの 4 大要素を全て提供しているプログラミング言語です。全てを満たしてこそ、オブジェクト指向プログラミング言語と言えます。
それでは、1 つずつ見ていきましょう。
5.1. カプセル化
カプセル化には、以下の 2 つのコンセプトがあります。
- オブジェクトごとに、変数とメソッドをまとめる。
- データをクラス内に書き、外から見えないようにする。
前者は、ここまで見てきたように、オブジェクト指向プログラミングは、「共通の属性を持つクラスごとに、固有の変数とメソッドを定義する」ということです。
後者は、オブジェクト指向プログラミングでは、「オブジェクト(インスタンス)のデータは、メソッドを通じて操作する」ということです。以下のコードをご覧ください。
まずは、Data クラスを作ります。
class Data:
'''初期化メソッド(インスタンス変数とデータ)'''
def __init__(self):
self.nums = [1, 2, 3, 4, 5]
'''インスタンスメソッド'''
def change_data(self, index, n):
self.nums[index] = n
次に、このクラスから 2 つのインスタンスを作り、インスタンス変数の値を変更します。
'''インスタンス変数 num をクラスの外側から直接変更する(client)。'''
data1 = Data()
data1.nums[0] = 100
print(data1.nums)
'''インスタンス変数 num をクラスで定義したメソッドを使って変更する。'''
data2 = Data()
data2.change_data(0, 100)
print(data2.nums)
どちらの方法でも、インスタンス変数 num の要素の値を変更することができています。
しかし、前者では、クラス内で定義したメソッドを使わずに、クラスの外側から直接書き換えています。このように、クラスの外側からオブジェクト(インスタンス)を操作するコードを client と言います。
client では、問題が起きる場合があります。例えば、変数 nums がタプル型だった場合、タプルはイミュータブルなオブジェクトなので、nums[0] = 100 のように、要素の一部を変更することはできません。そのため、プログラムが正しく動作しなくなります。
多くのプログラミング言語では、この問題に対して、プライベート変数や、プライベートメソッドというものを作り、オブジェクトの外から、参照や操作ができないようにして対処しています。しかし、Python では、全てのデータは、client から直接操作することができるパブリック変数です。
そのため、Python では、client からアクセスして欲しくない変数やメソッドには、名前の前にアンダースコア (_) を 1 つつけます。これによって、Python のプログラマーは、アンダースコアから始まっている変数やメソッドは、触ってはいけないものと認識します。
例えば、以下のようなコードですね。
class PublicPrivateExample:
'''初期化メソッド(インスタンス変数の定義)'''
def __init__(self):
self.public = "safe"
self._unsafe = "unsafe" #変数が _ で始まっています。
'''client が使っていいメソッド'''
def public_method(self):
pass
'''client が使うべきじゃないメソッド'''
def _unsafe_method(self): #メソッドが _ で始まっています。
pass
Python のプログラマーは、このコードを見ると、変数 self._unsafe と、メソッド _unsafe_method は使うべきではないと判断します。このように、Python では アンダースコアによって、使うべきか、使わないべきかを見分けるようになっています。
5.2. 抽象化
抽象化とは、「対象から小さな特徴を除いて、本質的な特徴だけを集めた状態にする」という手順です。
オブジェクト指向プログラミングでは、クラスでインスタンスの属性を定義する時に、不要な詳細を省略する時に、抽象化を使います。
例えば、人には、
- 肌の色
- 目の色
- 髪の色
- 身長
- 体重
- 性別
- 国籍
など、非常に多くの属性があります。しかし、人を、クラスで表現しようとした時、全ての属性が、そのクラスで扱いたい問題に関係があるわけではありません。
そこで、扱いたい問題だけに焦点を当てて、それに必要な属性だけを定義することで、人を抽象化してプログラムすることができます。
5.3. ポリモーフィズム
ポリモーフィズムとは、「同じインターフェイス(関数やメソッドのこと)でありながら、データの型に合わせて異なる動作をする機能」を提供するもののことです。
例えば、print()関数は、文字列型データや、整数型データ、浮動小数点数型データなど、異なるデータ型に対して実行することができますね。
print("Hello World") #文字列(str)型
print(100) #整数(int)型
print(100.1) #浮動小数点(float)型
もし、print()関数がポリモーフィズムでなかったら、それぞれのデータ型に合わせて、print_string, print_int, print_float のように、それぞれに専用の関数を用意する必要があります。
しかし、ポリモーフィズムのおかげで、print()という 1 つの関数だけで済みます。
もし Python がポリモーフィズムを適用していなかったら、ある関数やメソッドを使おうとしたら、まず対象となるデータの型を確認して、それに合った関数やメソッド名を呼ぶことが必要になります。そうなると、関数やメソッドを格納するファイルの種類も量も膨大なものになります。
プログラムが巨大になり、読むのも書くのも大変で、ちょっといじるだけで壊れやすくなりますし、機能を拡張するのも不可能に近くなります。
5.4. 継承
継承とは、「新しいクラスを作る時に、既存の他のクラスからメソッドや変数を受け継ぐことができる」というものです。
継承元となるクラスを親クラス、継承先のクラスを子クラスと言います。次の例をご覧ください。
'''親クラス'''
class Shape:
def __init__(self, w, l):
self.width = w
self.len = l
def print_size(self):
print(f"横は{self.width}cm, 縦は{self.len}cmです。")
'''子クラス'''
class Square(Shape):
pass
'''確認してみましょう。'''
square = Square(10, 10)
square.print_size()
子クラスの Square では、親クラスの Shape のインスタンス変数とインスタンスメソッドを継承しています。継承した結果、新たにコードを書かなくても、親クラスと同じ変数やメソッドを使うことができるようになっています。
これによって、同じコードを何度も書くという手間から解放され、コードの量を削減し、コード全体をより小さく簡潔に保つことができるようになります。
もちろん、子クラスには、子クラス独自の変数やメソッドを定義することもできます。子クラスで、新たに定義した変数やメソッドは、親クラスには影響しません。
以下に例を示しておきます。
'''親クラス'''
class Shape:
def __init__(self, w, l):
self.width = w
self.len = l
def print_size(self):
print(f"横は{self.width}cm, 縦は{self.len}cmです。")
'''子クラス'''
class Square(Shape):
def area(self):
return self.width*self.len
'''確認してみましょう。'''
square = Square(10, 10)
square.area()
また、メソッドオーバライドと言って、子クラスで、親クラスと同じ名前のメソッドを書くと、子クラス内でのメソッドの動作を書き換えることができます。これも親クラスには影響しません。
以下に例を示しておきます。
'''親クラス'''
class Shape:
def __init__(self, w, l):
self.width = w
self.len = l
def print_size(self):
print(f"横は{self.width}cm, 縦は{self.len}cmです。")
'''子クラス'''
class Square(Shape):
def print_size(self):
area = self.width * self.len
return f"横は{self.width}cm, 縦は{self.len}cm、面積は{area}㎠です。"
'''確認してみましょう。'''
square = Square(10, 10)
square.print_size()
6. まとめ
オブジェクト指向プログラミングは、手続き型プログラミングで、多くのプログラマーを悩ませたグローバル変数による不具合を解決するために生まれたものです。
オブジェクト指向プログラミングでは、オブジェクトの共通属性を、インスタンス変数として、クラス内で定義します。そして、クラスからインスタンスを作る時に、変数に値を与える形をとります。その時、同時に、そのクラスに共通のインスタンスメソッドも定義することができます。
まずは、このことをしっかりと理解しておきましょう。
それに加えて、オブジェクト指向プログラミングの 4大要素である、
- カプセル化
- 抽象化
- ポリモーフィズム
- 継承
を理解すると、オブジェクト指向プログラミングが、コードの再利用を促進し、システムの開発や保守にかかるトータル時間を短縮できるという特徴が、いかに優れた利点であるかが分かるようになります。オブジェクト指向プログラミングは、プログラムを書く前の計画や設計が、非常に重要なので、その手間が割に合わないと感じる方もいるかもしれません。
しかし、プログラミングの本質は、書くことよりも、考えることにあります。コードを書く前に、しっかりと計画や設計を考えることで、新しい発想が生まれることもあります。
オブジェクト指向プログラミングで、ぜひ、そうした経験も積んでいければ、とても良いですね。
コメント
コメント一覧 (13件)
平易ながらも本質的で素晴らしい記事でした。
pythonやプログラミングへの理解に加えて、文章力も高いんだろうな、と。。素人の私にもすっと入って理解できました。
とても参考になりました。やらせコメントみたいですが(笑)
不明用語に突き当たる → 調べる → 再び不明用語に突き当たる…(の繰り返し)
『で、最初は何を調べていたんだっけ』とならない。
要は、文書力が高くコンパクトにまとめ、(余計な)コマーシャルリンクが無い。 理解できたきになれました。感謝します。
最後のサンプルコードが、
そのひとつ前と同じ内容ではないでしょうか?
ご指摘ありがとうございます。
サンプルコードを修正させて頂きました。
不精なもので大変遅くなってしまいましたが、感謝いたします。
「独学プログラマー」のコピペ
ご指摘ありがとうございます。確かにその通りですね。
記事の冒頭でも、ご指摘を頂いた旨を正直に書き、『独学プログラマー』へ促すことで対応させて頂ければと思います。
独学プログラマーの内容をそのままコピーしたような内容ですね。
ご指摘ありがとうございます。確かにその通りです。
上のコメントへの回答の通り、記事の冒頭でも、ご指摘を頂いた旨を正直に書き、『独学プログラマー』へ促すことで対応させて頂ければと思います。
大好きです.
オブジェクト指向について大変勉強になりました。
独学プログラマーという書籍の写しだそうですが、それを知るのもネットサーフィンでここに辿りついたおかげなので素直に感謝です。
いずれソース元も参照したいと思います。
4. オブジェクト指向プログラミングの冒頭の説明で
「オブジェクト指向プログラミングも、手続き型プログラミングのように、グローバルステートを排除しているということは同じです。」
とありますが、”手続き型プログラミングのように”ではなく、”関数型プログラミングのように”ではないですか?
ご指摘ありがとうございます。その通りですね。早速修正させて頂きました。
「独学プログラマー」と言う書籍に対する著作権侵害に当たる可能性があるのではありませんか?
他者の著作物の引用をする場合は、その引用の出所の明示をしなければならず、引用する際には、原文を変えてはならないとされています。
サイトリンクを貼りしたので、リンクをご参照いただければと思います。