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という。

In [1]:
class Dog:
    def __init__(self, name): #propertyの定義
        self.name = name
 

 defはfucntionの宣言文なので、__init__という名前で、selfnameという引数のfunctionを定義したことになる。この__init__のような前後にアンダーバー二個づつ(計4個)でくくられたものは特殊メソッドという特別なfunctionで、propertyの宣言に使う。なおこのdef __init__で始まる文のことをconstructor(コンストラクター)という。selfとは自分自信のことで、後で説明するインスタンス(instance)自身のことを表している。

In [2]:
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.nameruns.という文字列を画面上にprintするだけのmethodであるが、とりあえずDog classができた。

 

 このようにclassを作ってみたものの、上のセルを実行してもなにも起こらない。なぜならclassは単なる設計図であって、まだ具体的な実体を作っていないからだ。犬とはどういうものか定義しただけで、まだ一匹の犬もこの世に作りだしていない状態である。classの設計図を基に作られる具体的実体のことをインスタンス(instance)という。instanceという言葉はあまり日本語になっていないので耳慣れないかもしれないが、日本語でいうと「例」という意味で、 例えば、for instanceはfor exampleと同じ意味。このinstanceのことをオブジェクト(object)ともいう。オブジェクト指向のオブジェクトである。

 classという設計図を基に、実体であるinstanceを一つ作ってみる。なお、先に述べたようにしきたり的にclassの頭文字は大文字にするが、instanceの頭文字は小文字にする。

In [3]:
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"ということになる。dog1nameというpropertyに、Cocoという変数を代入しなさいということだ。そこで、

In [4]:
print(dog1.name)
 
Coco
 

とすると、Cocoと表示され、dog1というinstanceのnameというpropertyはCocoになっていることが分かる。

 

 またDogにはrunsというmethodがあるので、以下を実行すると、

In [5]:
dog1.runs()
 
Coco runs.
 

という風に、Dogクラスのruns methodが実行されて、Coco runsと表示される。

 nameは、そのinstanceに固有な変数であるが、例えばすべての犬に共通する性質(足は4本)なども、以下のように定義できる。

In [6]:
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になる。

In [7]:
dog1 = Dog("Coco")
print(dog1.n_of_legs)
 
4
 

 なお、def __init__...のことをコンストラクター(constructor)と呼び、constructorの__init__のようにアンダーバー2個__で囲われたものことを特殊メソッドと呼ぶ。他にもいろいろ特殊メソッドはあるが、まあ__init__だけでもとりあえずはいいだろう。

 

 当然のことながらinstance変数は、あとから書き換えることもできる。

In [8]:
print(dog1.name)
dog1.name = 'Choco'
print(dog1.name)
 
Coco
Choco
 

 dog1.nameというDog classのinstance変数に、Chocoという名前を2行目で代入しているので、printすれば同然結果はChocoとなる。

 

 

 以上重要な概念/用語をまとめると、

– classは設計図のこと。

– instanceあるいはobjectは実体のこと。

.で属性(attribute)を表す。

– propertyはデータや値を表す属性のこと。

– methodは動作や機能を表す属性のこと。

 実はpythonでは、functionもobjectだし、instanceもinstance objectというobjectで、すべてがobjectであることは以下のコードからも分かる。

In [9]:
class MyClass():
    pass #これはなにもしないという意味(文法上これを書かないとエラーになる)
m = MyClass()
type(m)
Out[9]:
__main__.MyClass
 

 

クラスの名前空間(Namespace)

 functionの説明のところで、namespaceの概念を紹介した。namespaceはfunctionだけでなく、classやmoduleにも当てはまる。異なるclassや異なるmoduleにあるobjectで使われている変数は、外に出られないので、同じ名前であっても何の関係もない、ということだ。

 

 このように、MyClassというclassは、__main__という特殊なclassの属性になっていることが分かる。

 

 

クラスの継承(inheritance)

 

 すでに存在するclassを継承して新しいclassを作ることもできる。例えば、

In [10]:
class Animal:
    n_of_eyes = 2
    def __init__(self, name): #constructor
        self.name = name
    def walk(self):
        print(self.name, 'can walk.')
 

というclassから、

In [11]:
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を実行してみる。

In [12]:
bird1 = Bird("Bob")
bird1.fly()
bird1.walk()
bird1.n_of_eyes
 
Bob can fly.
Bob can walk.
Out[12]:
2
 

というように、”Bob”という名前の鳥はBird classのmethodであるflyと、そのsuper classであるAnimal classのmethodであるwalkを持っていることが分かる。また、Animal classのクラス変数n_of_eyesも継承されていることが確認できる。

 

Attribute(属性)まとめ

 

 Classの概念が分かったところで、属性(attribute)についてまとめておく。属性とは、a.realのように、ドット.以下で表されるデータや値、あるいは機能のことだと以前説明した。

 NumPyを利用するとき、たとえば、

In [13]:
import numpy as np
print(type(np))
print(type(np.pi))
np.pi
 
<class 'module'>
<class 'float'>
Out[13]:
3.141592653589793
 

とすると、npとは”module”という”class”であり、np.piとはNumPy moduleのfloatという属性であることが分かる。

 作図をするときに使ったmatplotlibもmoduleである。下のような例で、

In [15]:
import matplotlib.pyplot as plt
m = np.array([[1, 2, 3],
              [2, 2, 3]])
plt.imshow(m)
plt.colorbar();
 
In [16]:
print(type(plt))
print(type(plt.imshow))
print(type(plt.colorbar))
 
<class 'module'>
<class 'function'>
<class 'function'>
 

 pltとはmatplotlib.pyplotというmoduleを省略してpltとしたmoduleであり、imshowcolorbarは、pltのfunctionという属性であることが分かる。

 

 SciPyをimportするときに、

In [17]:
from scipy import pi
import numpy as np
print(pi)
print(np.pi)
 
3.141592653589793
3.141592653589793
 

 という書き方をした。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の定義を行う。

In [20]:
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を作って表示させてみる。

In [21]:
m = Make1Darray(10)
print(m.x)
print(m.x2)
 
[0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.  1.1 1.2 1.3 1.4 1.5 1.6 1.7
 1.8 1.9 2.  2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3.  3.1 3.2 3.3 3.4 3.5
 3.6 3.7 3.8 3.9 4.  4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 5.  5.1 5.2 5.3
 5.4 5.5 5.6 5.7 5.8 5.9 6.  6.1 6.2 6.3 6.4 6.5 6.6 6.7 6.8 6.9 7.  7.1
 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9 8.  8.1 8.2 8.3 8.4 8.5 8.6 8.7 8.8 8.9
 9.  9.1 9.2 9.3 9.4 9.5 9.6 9.7 9.8 9.9]
[0.000e+00 1.000e-02 4.000e-02 9.000e-02 1.600e-01 2.500e-01 3.600e-01
 4.900e-01 6.400e-01 8.100e-01 1.000e+00 1.210e+00 1.440e+00 1.690e+00
 1.960e+00 2.250e+00 2.560e+00 2.890e+00 3.240e+00 3.610e+00 4.000e+00
 4.410e+00 4.840e+00 5.290e+00 5.760e+00 6.250e+00 6.760e+00 7.290e+00
 7.840e+00 8.410e+00 9.000e+00 9.610e+00 1.024e+01 1.089e+01 1.156e+01
 1.225e+01 1.296e+01 1.369e+01 1.444e+01 1.521e+01 1.600e+01 1.681e+01
 1.764e+01 1.849e+01 1.936e+01 2.025e+01 2.116e+01 2.209e+01 2.304e+01
 2.401e+01 2.500e+01 2.601e+01 2.704e+01 2.809e+01 2.916e+01 3.025e+01
 3.136e+01 3.249e+01 3.364e+01 3.481e+01 3.600e+01 3.721e+01 3.844e+01
 3.969e+01 4.096e+01 4.225e+01 4.356e+01 4.489e+01 4.624e+01 4.761e+01
 4.900e+01 5.041e+01 5.184e+01 5.329e+01 5.476e+01 5.625e+01 5.776e+01
 5.929e+01 6.084e+01 6.241e+01 6.400e+01 6.561e+01 6.724e+01 6.889e+01
 7.056e+01 7.225e+01 7.396e+01 7.569e+01 7.744e+01 7.921e+01 8.100e+01
 8.281e+01 8.464e+01 8.649e+01 8.836e+01 9.025e+01 9.216e+01 9.409e+01
 9.604e+01 9.801e+01]
 

 となって答えが出た。

 注意点として、mが配列であると勘違いしている人が入門者に多い。typeすると違いが確認できる。

In [22]:
print(type(m.x))
print(type(m))
 
<class 'numpy.ndarray'>
<class '__main__.Make1Darray'>
 

 m.xは、np.ndarrayというclass(つまり配列)である。一方、mは、instanceであり、コードのメインの部分を表す__main__Make1Darrayという属性だということができる。だから、np.ndarrayの属性の一つである.shapeは、mには使用できない。つまり、

In [23]:
print(m.x.shape)
 
(100,)
 

は正しいが、

In [24]:
print(m.shape)
 
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-24-ebb5e5d6c5e6> in <module>
----> 1 print(m.shape)

AttributeError: 'Make1Darray' object has no attribute 'shape'
 

は正しくないので、pythonに怒られる。両者の違いは明らかなのだが、よくオブジェクト指向をよく理解している中級者でも、instanceそのものにnp.ndarrayの属性を適応しようとしてエラーになっていることに気づかずにハマる、ということが結構多いので注意。

 

 

(1.12.2.) Sine classの定義を行う。fを新たに引数としてdef __init__に追加する。super()によって、super classを継承しているので、super classで定義されているself.xはそのまま使用できる。

In [25]:
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.sinxm.sinx2を、super classから継承した属性m.xに対してプロットすればよい。

In [26]:
f = 0.05
m = Sine(10*np.pi, f)
plt.plot(m.x, m.sinx)
plt.plot(m.x, m.sin2x);
 
 

 

以上のようにclassをわざわざ使うこともできるが、当然ながら同じことはclassを使わずに以下のように手続き型指向的に書くこともできる。

In [27]:
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