1.12. オブジェクトとクラス¶
オブジェクト指向は現代のプログラミングにおいて欠かすことできない概念で、世の中で使われているほとんどのプログラミング言語はオブジェクト指向といっても過言ではない。例外的にC言語はオブジェクト指向ではないがCにオブジェクト指向を追加したものがC++である。数値計算のコードを一人で書く場合には、オブジェクト指向の対となる概念である「手続き型指向」で書くこともできるが、大規模なコードや複数の人で分担してコードを書く場合には、オブジェクト指向のご利益は絶大だ。せっかくpythonを勉強しているのに「オブジェクト指向がわからない」のはちょっと恥ずかしいので、本講義でもオブジェクト指向プログラミングについて少し説明する。より高度な内容については適宜専門書を読むことをお勧めする。
以前、pythonには基本的な型(データタイプ)が四つあるという話をした。int型、float型、string型、bool型だ。数字やテキスト文など、簡単な変数を扱うだけならそれだけでも問題ない。しかし、もっと現実的なものを対象物にするとき、例えば「犬」とか「りんご」とか「車」とか「歩く」とか「消す」などをコンピューター上で表したいときは、さきほどの四つの基本型ではもちろん表すことはできない。
ではどう表したらいいだろうか。神様か創造主になったつもりで、コンピューター上で「犬」を創造してみよう。例えば「犬」という抽象概念を具現化するのに、その犬の「犬種」、「年齢」、「体重」、「毛の色」、「雄雌」などの属性を持たせたらどうだろうか。「犬種」ならstring型で表せられるし、「年齢」なら、int型で表せる。「毛の色」の場合でも、白を0
, 赤を1
, 黒を2
などと対応させればint型で表せるだろうし、白をwhite
、赤をred
とすればstring型にしてもいい。雌をTrue
、雄をFalse
を入れれば「雄雌」はbool型で表せる。もちろん0
, 1
でもいい。このような性質やデータを表す属性(attribute)のことをpythonではプロパティ(property)と呼ぶ。(言語によって呼び名が異なり、Javaではこれをフィールドと呼ぶ。)
一方、同じ属性でも「動作」あるい「処理」を表したい場合もある。このような属性のことをメソッド(method)と呼ぶ。例えば「犬」は「走る」「喜ぶ」というmethodを持っているが、「飛ぶ」というmethodは持ってない。「飛行機」は「喜ぶ」というmethodは持っていないが、「飛ぶ」は持っているなどなど。methodに関しては、def
を使って例えば「走る」というfunctionを定義してやればいい。このように属性を自分の好きなようにデザインしていけば自分なりの「犬」という概念が設計できる。
このようにして自分で作った「犬」に対応する抽象的な概念を、基本型であるint type、float typeなどの拡張版という意味で、犬”type”と呼んでもいいが、プログラミング言語ではこれをクラス(class)と呼ぶ。たとえば、「犬 class」とか「リンゴ class」とか呼ぶ。変数の基本型の話をしたとき、型のことをclassとも呼ぶといったのは、classという概念が型(date type)を拡張したものだからだ。
実際に「犬」classを具体的に設計するには、そのclassがどのような属性を持っているか決める必要がある。いわば「犬」の設計図である。これをpythonで表現してみよう。クラスはclass
と文末のコロン(:
)によって宣言する。classの中身もインデントをつけて書くのはfunctionなどと同じである。なお、エラーにはならないがしきたりとしてclass名の頭文字は大文字で書く。このしきたりのことをPEP8という。
class Dog:
def __init__(self, name): #propertyの定義
self.name = name
def
はfucntionの宣言文なので、__init__
という名前で、self
とname
という引数のfunctionを定義したことになる。この__init__
のような前後にアンダーバー二個づつ(計4個)でくくられたものは特殊メソッドという特別なfunctionで、propertyの宣言に使う。なおこのdef __init__
で始まる文のことをconstructor(コンストラクター)という。self
とは自分自信のことで、後で説明するインスタンス(instance)自身のことを表している。
class Dog:
def __init__(self, name):
self.name = name
def runs(self): #methodの定義
print(self.name, 'runs.')
続いてmethodを定義するには、4, 5行目を追加する。def runs(self):
は引数が自分自身(self
)しかないfunctionを定義していて、このfunctionは、self.name
とruns.
という文字列を画面上にprintするだけのmethodであるが、とりあえずDog
classができた。
このようにclassを作ってみたものの、上のセルを実行してもなにも起こらない。なぜならclassは単なる設計図であって、まだ具体的な実体を作っていないからだ。犬とはどういうものか定義しただけで、まだ一匹の犬もこの世に作りだしていない状態である。classの設計図を基に作られる具体的実体のことをインスタンス(instance)という。instanceという言葉はあまり日本語になっていないので耳慣れないかもしれないが、日本語でいうと「例」という意味で、 例えば、for instanceはfor exampleと同じ意味。このinstanceのことをオブジェクト(object)ともいう。オブジェクト指向のオブジェクトである。
classという設計図を基に、実体であるinstanceを一つ作ってみる。なお、先に述べたようにしきたり的にclassの頭文字は大文字にするが、instanceの頭文字は小文字にする。
dog1 = Dog("Coco")
上のセルを実行しても、なにも表示されないが、この一文でdog1
という具体的な実体、つまりCoco
という名前を持った一匹の犬がコンピューター上に作られた。
インスタンス名がdog1
で、Dog
の引数は"Coco"
である。この"Coco"
という引数は、Dog
クラスの定義の2行目のdef __init__(self, name)
のname
に引き渡される。ここで__init
の__
はアンダーバーを二つ書いたものである。self
とはinstance自身のことで、いまの場合dog1
のことである。このつまり、3行目にあるself.name = name
というのは、dog1.name = "Coco"
ということになる。dog1
のname
というpropertyに、Coco
という変数を代入しなさいということだ。そこで、
print(dog1.name)
とすると、Coco
と表示され、dog1
というinstanceのname
というpropertyはCoco
になっていることが分かる。
またDog
にはruns
というmethodがあるので、以下を実行すると、
dog1.runs()
という風に、Dog
クラスのruns
methodが実行されて、Coco runs
と表示される。
name
は、そのinstanceに固有な変数であるが、例えばすべての犬に共通する性質(足は4本)なども、以下のように定義できる。
class Dog:
n_of_legs = 4 #この一行を追加した
def __init__(self, name): #constructor
self.name = name
def runs(self): #methodの定義
print(self.name, 'runs')
n_of_legs
のように、def __init .... :
の外に書いた変数は、classに共通する変数となる。これをクラス変数(class veriable)という。一方、.name
のようなinstanceに固有の変数はインスタンス変数(instance veriable)という。class変数はinstanceによらないので、どのようなinstanceを作っても、n_of_legs
というpropertyは4
になる。
dog1 = Dog("Coco")
print(dog1.n_of_legs)
なお、def __init__...
のことをコンストラクター(constructor)と呼び、constructorの__init__
のようにアンダーバー2個__
で囲われたものことを特殊メソッドと呼ぶ。他にもいろいろ特殊メソッドはあるが、まあ__init__
だけでもとりあえずはいいだろう。
当然のことながらinstance変数は、あとから書き換えることもできる。
print(dog1.name)
dog1.name = 'Choco'
print(dog1.name)
dog1.name
というDog
classのinstance変数に、Choco
という名前を2行目で代入しているので、print
すれば同然結果はChoco
となる。
以上重要な概念/用語をまとめると、
– classは設計図のこと。
– instanceあるいはobjectは実体のこと。
– .
で属性(attribute)を表す。
– propertyはデータや値を表す属性のこと。
– methodは動作や機能を表す属性のこと。
実はpythonでは、functionもobjectだし、instanceもinstance objectというobjectで、すべてがobject
であることは以下のコードからも分かる。
class MyClass():
pass #これはなにもしないという意味(文法上これを書かないとエラーになる)
m = MyClass()
type(m)
クラスの名前空間(Namespace)¶
functionの説明のところで、namespaceの概念を紹介した。namespaceはfunctionだけでなく、classやmoduleにも当てはまる。異なるclassや異なるmoduleにあるobjectで使われている変数は、外に出られないので、同じ名前であっても何の関係もない、ということだ。
このように、MyClass
というclassは、__main__
という特殊なclassの属性になっていることが分かる。
クラスの継承(inheritance)¶
すでに存在するclassを継承して新しいclassを作ることもできる。例えば、
class Animal:
n_of_eyes = 2
def __init__(self, name): #constructor
self.name = name
def walk(self):
print(self.name, 'can walk.')
というclassから、
class Bird(Animal):
def __init__(self,name):
super().__init__(name)
def fly(self):
print(self.name, 'can fly.')
というBird
classを作る。name
は継承元のclassから継承されている。継承元のclassをsuper classや親クラスといい、継承するclassをsub classや子クラスという。 Bird
classからinstanceを作ってmethodを実行してみる。
bird1 = Bird("Bob")
bird1.fly()
bird1.walk()
bird1.n_of_eyes
というように、”Bob”という名前の鳥はBird
classのmethodであるfly
と、そのsuper classであるAnimal
classのmethodであるwalk
を持っていることが分かる。また、Animal
classのクラス変数n_of_eyes
も継承されていることが確認できる。
Attribute(属性)まとめ¶
Classの概念が分かったところで、属性(attribute)についてまとめておく。属性とは、a.real
のように、ドット.
以下で表されるデータや値、あるいは機能のことだと以前説明した。
NumPyを利用するとき、たとえば、
import numpy as np
print(type(np))
print(type(np.pi))
np.pi
とすると、np
とは”module”という”class”であり、np.pi
とはNumPy moduleのfloatという属性であることが分かる。
作図をするときに使ったmatplotlibもmoduleである。下のような例で、
import matplotlib.pyplot as plt
m = np.array([[1, 2, 3],
[2, 2, 3]])
plt.imshow(m)
plt.colorbar();
print(type(plt))
print(type(plt.imshow))
print(type(plt.colorbar))
plt
とはmatplotlib.pyplot
というmoduleを省略してplt
としたmoduleであり、imshow
やcolorbar
は、plt
のfunctionという属性であることが分かる。
SciPyをimportするときに、
from scipy import pi
import numpy as np
print(pi)
print(np.pi)
という書き方をした。1行目はscipy
というモジュールからpi
という属性をimportせよ、ということなので、ただ単純にpi
という変数で扱うことができた。2行目は、NumPy自体をごっそりimportしてnp
と省略せよ、としているので、np.pi
のような書き方をしないといけないわけである。
以前、functionの話をしたとき、namespaceの概念を紹介した。namespaceはfunctionだけでなく、classやmoduleでも当てはまる。異なるclassや異なるmoduleにあるobjectで使われている変数は、同じ名前であっても何の関係もない、ということだ。
classと属性の説明が終わったので、これで大体、数値計算コードをpythonで書くために必要な文法的な準備ができた。
練習問題¶
(1.12.1.) $0$から$n-1$まで$0.1$づつ増加する整数を要素に持つ1次元配列$x=(0, 0.1, \cdots, n-0.1)$と$x$の要素の二乗を要素に持つ一次元配列$x_2=(0, 0.01, \cdots, (n-0.1)^2)$を作りたい。もちろんx = np.arange(0, n, 0.1)
とx**2
で作ることができるが、これをclassを使って書きたい。一次元配列$x$を属性.x
で取得でき、$x_2$を.x2
で取得できるように、class Make1Darray
を作れ。また、$n=10$としてMake1Darray
classのinstanceを一つ作って中身を確認せよ。
(1.12.2.) 前問(1.12.1.)のMake1Darray
を継承して、$\sin (2 \pi f x)$と$\sin^2 (2 \pi f x)$を要素に持つ配列を作るためのSine
classを作成する。例えばSine
classのinstanceをm
としたとき、m.x
で$x$を、m.sin
で、$\sin(2 \pi f x)$を、m.sin2
で、$\sin^2 (2 \pi f x)$を取り出せるように属性を定義せよ。このclassを使って、$0 \leq x \leq 10 \pi$までの範囲で$~\sin (2 \pi f x)~$と$~\sin ^2(2 \pi f x)~$を図示せよ。ただし、$f = 0.05$とする。
解答例¶
(1.12.1.) Make1Darray
classの定義を行う。
import numpy as np
class Make1Darray:
def __init__(self, n):
self.x = np.arange(0, n, 0.1)
self.x2 = self.x**2
要素の二乗はself.x**2
とする。次に$n=10$としたinstancem
を作って表示させてみる。
m = Make1Darray(10)
print(m.x)
print(m.x2)
となって答えが出た。
注意点として、m
が配列であると勘違いしている人が入門者に多い。type
すると違いが確認できる。
print(type(m.x))
print(type(m))
m.x
は、np.ndarray
というclass(つまり配列)である。一方、m
は、instanceであり、コードのメインの部分を表す__main__
のMake1Darray
という属性だということができる。だから、np.ndarray
の属性の一つである.shape
は、m
には使用できない。つまり、
print(m.x.shape)
は正しいが、
print(m.shape)
は正しくないので、pythonに怒られる。両者の違いは明らかなのだが、よくオブジェクト指向をよく理解している中級者でも、instanceそのものにnp.ndarray
の属性を適応しようとしてエラーになっていることに気づかずにハマる、ということが結構多いので注意。
(1.12.2.) Sine
classの定義を行う。f
を新たに引数としてdef __init__
に追加する。super()
によって、super classを継承しているので、super classで定義されているself.x
はそのまま使用できる。
import matplotlib.pyplot as plt
class Sine(Make1Darray):
def __init__(self, n, f):
super().__init__(n)
self.sinx = np.sin(2*np.pi * f * self.x)
self.sin2x = self.sinx**2
5, 6行目で$\sin(x)$と$\sin^2(x)$を求め、それぞれ属性.sinx
、.sin2x
と定義している。次にinstance m
を作り、m
の属性m.sinx
とm.sinx2
を、super classから継承した属性m.x
に対してプロットすればよい。
f = 0.05
m = Sine(10*np.pi, f)
plt.plot(m.x, m.sinx)
plt.plot(m.x, m.sin2x);
以上のようにclassをわざわざ使うこともできるが、当然ながら同じことはclassを使わずに以下のように手続き型指向的に書くこともできる。
f = 0.05
x = np.arange(0,10*np.pi, .1)
y = np.sin(2*np.pi*f*x)
y2 = y**2
plt.plot(x, y)
plt.plot(x, y2);
簡単な数値計算などで計算結果を単純に求めたいだけならこちらの方が分かりやすくて簡単なのでオブジェクト指向やクラスがなぜ必要なのか習いたてのころは分からないかしれない。しかし一度作ったclassは再利用できて使いまわしができるし、何百何千というファイルで構成されているような本格的なコードではオブジェクト指向を使わないとほとんど書けない。
当サイトのテキスト・画像の無断転載・複製を固く禁じます。
Unauthorized copying and replication of the contents of this site, text and images are strictly prohibited.
© 2019 Go Yusa