今回から「オブジェクト指向」について、複数回にわたって取り扱います。「データ」と「処理」を別々のものとして扱ってきたのが今までの手続き型のプログラミングのスタイルですが、簡単にいってしまうと、それらをひとつのものとして扱うのがオブジェクト指向のプログラミングです。
オブジェクト指向の思想をきちんと理解するには、座学だけではなく経験が必要です。座学でなんとなく理解することは可能かもしれませんが、座学で学んだ知識で自分でコードを書き続けて、設計を間違えたり、それを直したり、はたまた座学を再びやってみたり……そういうことを繰り返しているうちに、なんとなく理解できるようになる傾向があります(少なくとも私が知る開発者たちを見る限りは)。
また、ひとえにオブジェクト指向といっても、人によってさまざまな細かい解釈があり、若干ボヤッとしたイメージもあります。そういった細かい哲学的な話は抜きにして、とりあえずオブジェクト指向の6割程度まで理解するということを、本連載の目標にします。
学習の流れ
複数回にわたってオブジェクト指向について解説するため、どういう順序で取り扱うのか、今後の流れを説明します。変更する可能性がありますが、おおまかに以下の順序で進めていく予定です。
まず前半で以下を扱います。
|
---|
最初に使い方を説明した後で、最後に「なぜそのようになっているのか」について解説します。
オブジェクト指向の後半では以下を扱います。
|
---|
正直なところ、初級者が自分で後半の内容の設計をすることはほとんどありません。ただ、別の人が作ったライブラリなどを利用する際に、これらの知識が必要となってくるため、使えるレベルになる必要があります。
なお、延々とこれらの説明をしていても面白くないと思うので、途中でオブジェクト指向を使って簡単なゲームやGUIのアプリケーションを作ってみたいと思います。
構造体
かなり前の回になってしまうのですが、タプルという型を扱ったことを覚えていますか。タプルは複数のデータをひとまとめにする型でした。
たとえばあるお店で会員情報を扱う際に「名前、生年月日、住所」を情報とする場合、以下のようにひとつにまとめられるのでした。
>>> a = ('taro','1986','tokyo')
>>> type(a)
<type 'tuple'>
ただ、タプルはあくまでも情報をまとめる手段であり、会員情報として扱うのはプログラマたちが「そういうふうに扱う」という自分ルールで実施します。そのため、たとえば名前と生年月日を逆に扱うといった、参照する順序を間違えるトラブルが発生したり、新しい入会年月日などの情報をタプルに追加した際に、関数呼び出しや戻り値の受け取りで不整合が発生したりすることがあります。タプルは簡単に使える一方、その簡単さに起因したトラブルに注意しなければなりません。
オブジェクト指向でいうクラスのベースとなった「C言語の構造体」はタプルと似ているものの、以下の3点が異なります。
- 構造体はタプルのような汎用型ではなく専用の型を作る
- 構造体は順番ではなく名前でデータを参照
- 構造体は中身のデータを更新可能
以下にタプルと構造体の違いを図にまとめます。
実際に両者を比べてみます。
まずPythonのタプルです。
taro = ('taro', 1986,'tokyo')
print('1: {} {} {}'.format(taro[0], taro[1], taro[2]))
taro = (taro[0], 1990, taro[2])
print('2: {} {} {}'.format(taro[0], taro[1], taro[2]))
# 1: taro 1986 tokyo
# 2: taro 1990 tokyo
注目してほしいのは独自の型を使うのではなく、タプルとして()でくくったデータ群を作り、それを会員情報taroとしています。値の取り出しも順番を指定して参照しています。再代入はできないため、新しくタプルを作りなおすことで更新しています。
次にC言語の構造体です。
#include <stdio.h>
struct userInfo{
char name[100];
int birth;
char address[100];
};
int main(void){
struct userInfo taro = {"taro", 1986, "tokyo"};
printf("1: %s %d %s\n", taro.name, taro.birth, taro.address);
taro.birth = 1990;
printf("2: %s %d %s\n", taro.name, taro.birth, taro.address);
}
// 1: taro 1986 tokyo
// 2: taro 1990 tokyo
C言語の連載ではないので詳細は省きますが、注目して欲しいのはstruct userInfoとして構造体で「新しい型」を定義していることです。ここではuserInfoが型名で、nameやbirthはその構造体が持つデータです。そして変数taroは、userInfo型のデータを格納しています(Cに詳しい人が見ると突っ込みどころのある説明かと思いますが、ご容赦ください)。
上記の型の定義の後は、普通のintなどの型と大きな違いはありません。両者を比べてみるとよくわかります。
定義したuserInfo型の初期化では、タプルと記号は違うものの似たようにして{}に 構造体の要素を順に並べています。ただ、大きく異なるのが参照と代入です。例を見てもらうとわかるように、変数に代入された構造体が持つ各データへの参照は「変数名.データ名」となっています。Pythonのタプルでは[0]や[1]といったように何番目のデータか指定することでアクセスしていましたが、それに比べるとわかりやすいですね。
クラスを構造体のように使う
構造体を使うには「型の定義」と「作った型のデータを作成」という2つの手順を踏むのでした。あいにくPythonには構造体はないのですが、代わりに構造体を発展させた「クラス」と呼ばれる機能を使うことができます。クラスは簡単にいってしまうと「構造体に関数をもたせたもの」といえるのですが、詳細は次回以降にまわして、今回はとりあえずクラスを構造体のように使う方法についてお話します。
クラスの定義も、構造体と同じように「クラスの名前」と「クラスが持つデータ」を宣言します。それは、以下のようになります。
class UserInfo:
def __init__(self):
self.name = ''
self.birth = 0
self.address = ''
クラスは構造体よりも高機能なため、少し複雑になっていますが、class UserInfoでクラス名を宣言し、そのなかの関数の定義のような箇所でname、birth、addressという変数を作成しています。def __init__やselfがなんなのかについては、おそらく次回にお話しますので、今回はとりあえずそういうものなのだと捉えてください。
構造体のようにデータを作ることもできますが、その方法は異なってきます。
taro = UserInfo()
print('1: {}, {}, {}'.format(taro.name, taro.birth, taro.address))
# 1: , 0,
上記のように「クラス名()」とすることで、そのクラスから実際のデータを作ることができます。作られたデータを確認すると、クラスの定義の中で書かれた初期値を出力していることがわかりますね。なお、クラスから作成されたデータを「インスタンス」と呼びます。
このインスタンスに対して、構造体と同じように名前を使うことで、各データに対してアクセスできます。インスタンスが持つデータのことを「インスタンス変数」と呼びます。name、birth、addressはインスタンス変数です。Pythonではあまり聞きませんが、インスタンス変数はフィールドという呼ばれ方もします。
taro = UserInfo()
taro.name = 'taro'
taro.birth = 1986
taro.address = 'tokyo'
print('2: {}, {}, {}'.format(taro.name, taro.birth, taro.address))
# 2: taro, 1986, tokyo
print(type(UserInfo))
print(type(taro))
# <type 'classobj'>
# <type 'instance'>
インスタンスを作成した後にインスタンス変数をセットしていくのではなく、構造体のように最初から 'taro'、1986、'tokyo'などという値をインスタンスにセットさせることも可能です。ただ、もう少しクラスについて理解する必要があるので、それについてもまた次回以降の解説とさせてください。
クラスとインスタンスの関係
構造体とクラスについて学んできましたが、今日の段階で覚えておいてもらいたいことは以下の3点だけです。
- クラスは雛形で設計図のようなもの
- インスタンスはクラスから作られる実体
- インスタンスにどのようなデータを持たせるかはクラスで定義する
両者はちょうど、たい焼きの型枠とたい焼きのような関係かもしれません。たい焼きはその枠型という設計図に従って量産されますよね。どういうたい焼きを焼きたいかによって、そのもとになる「たい焼きの型枠」が変わってきます。いったん型枠さえ作ってしまえば、何個でも実体としてのたい焼きが作れます。
クラスもこれと同じで、どういうようなデータや処理を持ちたいかによって、どういうクラスを作るかが変わってきます。そしていったんクラスを作ってしまえば、インスタンスは好きなタイミングで好きな数だけ作ることが可能です。
両者の関係のイメージ図を以下に記載します。
演習1
タプルに比べて構造体が優れている点を述べてください。
演習2
以下の用語について説明してください。
- クラス
- インスタンス
- インスタンス変数
記事の本文から探すだけではなく、検索するなどしていろいろな情報を得てもらいたいです。
演習3
生徒の成績を保持するクラスを作ります。以下の条件でクラスを作成してください。
- クラス名 Score
- 生徒の名前を保持するインスタンス変数nameを作成
- 国数英の変数であるmath, english, japaneseを作成
演習4
数学mathの成績が一番良い生徒の名前を表示するプログラムを書いてください。なお、以下にテスト用のコードがありますので、必要であればコピペして使ってください。
taro = Score()
taro.name = 'taro'
taro.math = 60
taro.english = 70
taro.japanese = 80
jiro = Score()
jiro.name = 'jiro'
jiro.math = 80
jiro.english = 70
jiro.japanese = 90
saburo = Score()
saburo.name = 'saburo'
saburo.math = 50
saburo.english = 30
saburo.japanese = 60
ヒント: リストから最大値を探すプログラムを説明したことがあります。生徒のリストを作成すれば、同じ要領で処理できるはずです。
演習5(必須ではない)
「名前付きタプル(namedtuple)」という型を使うと、Cの構造体に近いことができます。これについて調べて、演習3~4の内容を名前付きタプルで実現してください。
今回はクラスを構造体として利用する方法について学びました。またその過程で、クラスとインスタンスの関係についても扱いました。次回は、クラスと構造体の違いについて解説します。
執筆者紹介伊藤裕一(ITO Yuichi)シスコシステムズでの業務と大学での研究活動でコンピュータネットワークに6年関わる。専門はL2/L3 Switching とデータセンター関連技術およびSDN。TACとしてシスコ顧客のテクニカルサポート業務に従事。社内向けのソフトウェア関連のトレーニングおよびデータセンタとSDN関係の外部講演なども行う。 もともと仮想ネットワーク関連技術の研究開発に従事していたこともあり、ネットワークだけでなくプログラミングやLinux関連技術にも精通。Cisco社内外向けのトラブルシューティングツールの開発や、趣味で音声合成処理のアプリケーションやサービスを開発。 Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009年度 IPA 未踏プロジェクト採択 詳細(英語)はこちら |
---|