🦎

Python のクラス

8 min read

クラスとは

Python に限らず現代のプログラミング言語[1]において、すべてのプログラムは状態(status)と手続き(procedure)から構成されます。Python のクラス(class)とは、状態と手続きをまとめる方法のひとつです。

状態とは

状態とは、数値や文字列など、何らかの情報を指します。Python では変数(variable)がプログラムの「状態」に当たる概念です。プログラムは、スイッチは ON/OFF のどちらなのか、いま何時なのか、いま気温は何℃なのかといった様々な情報によって処理を分岐しますが、そのために保存している時間や外気温の情報を「状態」と呼びます。

# Python における状態の概念の例

x = 3.14
y = 'dog'
z = [1, 2, 3]

手続きとは

手続きとは、状態を別の状態に変化させる何らかの操作を指します。Python では関数(function)や演算子(operator)がプログラムの「手続き」に当たる概念です。現代の計算機ではより複雑な処理を実現するために条件分岐if文)と繰り返し処理for文)がプリミティブな手続きとして用意されています。

手続き自体は、本来は状態を持ちません。以下の例を見てください。

# Python における手続きの概念の例

def quad(x):
    return x * x

quad関数は外から入ってきた x というデータに、x * x という操作を施して外に戻すという処理を表現しており、quad関数の状態(たとえば今までに何回実行されたのかなど)を示すような情報は持っていません。

C言語のように原始的な言語では、手続きはあくまでも「状態に操作を施すもの」であって、手続き自体になんらかの操作を施すことは考えられません。たとえば関数にいくつかの実行モードがあったとして、関数の引数で指定する以外には外からその実行モードを切り替える方法は基本的にありません。

逆に、状態自体は、本来は手続きを持ちません。2という値があるとき、それ自体が何かの処理や操作を表すことはなく、あくまでも「操作される対象」です。

クラスとは

Python のように高度化した現代のプログラミング言語では、状態と手続きをうまくまとめる概念が用意されており、Python ではクラスがそれに当たります。関数にいくつかの実行モードがあるときは外から切り替えができたほうが便利ですし、数値データだって操作対象になるだけではなく「自分自身の値をファイルに保存する」という機能がついていたほうが便利に決まっています。この要望をとても素直な方法で実現するのがクラスです。たとえば、

# 関数をハイスピードモードに切り替える
function.mode = 'high speed'

# 数値データを指定したファイルに保存する
value.save('data.csv')

といった書き方を提供します。

クラスの役割は、主に以下の 2 つの機能を提供することです。

  1. 対象の名前を起点に、その対象が持つ状態にアクセスする
  2. 対象の名前を起点に、その対象が持つ手続きにアクセスする

つまり対象が、状態と手続きの両方の性質を持ってしまえばいいという発想がクラスの考え方です。もっとも簡単なクラスを定義してみます。

# クラスを定義する構文です
class View:
    x = 0    # 対象が持つ「状態」です
    
    def show(self):    # 対象が持つ「手続き」です
        print(self.x)

クラスの定義は設計図みたいなものなので、実際に使うには以下の構文でメモリ上に実体化しなければなりません。メモリ上に実体化されたクラスはインスタンスと呼ばれます。

# 右辺で生成されたインスタンスに左辺にある v という名前をつけます
# 以降は v という名前でこのインスタンスにアクセスすることができます。
v = View()

インスタンスが持つ状態には . でアクセスすることができます。

# インスタンスが持つ状態にアクセスし、それを print 関数で表示します
print(v.x)

# インスタンスが持つ状態を書き換えます
v.x = 5

print(v.x)

インスタンスが持つ手続きにも、同様にして . でアクセスすることができます。

# インスタンスが持つ手続きにアクセスし、それを実行します
v.show()

もう少し丁寧に解説

先ほどさらりと書いた

「クラスの定義は設計図みたいなものなので、実際に使うには以下の構文でメモリ上に実体化しなければなりません。メモリ上に実体化されたクラスはインスタンスと呼ばれます。」

という文章を初めて見た人は何を言っているのかさっぱりわからなかったと思うので、もう少し丁寧に説明します。まずイメージを伝えると、クラスとインスタンスの関係は、たいやきの型とたいやきの関係と同じです。

# たいやきの型をつくる
class Taiyaki:
    nakami = None    # 対象が持つ「状態」です
    
    def show(self):    # 対象が持つ「手続き」です
        print(self.nakami)

# 型を使って1つ目のたいやきを焼く
taiyaki_1 = Taiyaki()
# 型を使って2つ目のたいやきを焼く
taiyaki_2 = Taiyaki()

# 1つ目のたいやきの中につぶあんを詰める
taiyaki_1.nakami = 'つぶあん'
# 2つ目のたいやきの中にこしあんを詰める
taiyaki_2.nakami = 'こしあん'

# 1つ目のたいやきを割ってみる。中身はつぶあん。
taiyaki_1.show()
# 2つ目のたいやきを割ってみる。中身はこしあん。
taiyaki_2.show()

普通は焼くときにあんを詰めるので焼いてから詰めるのはどうかと思いますが、でもまぁ、イメージはつくでしょうし、あとで焼くときに詰める方法も紹介します。

コンピュータの中でもたいやきと似たようなことが起こっています。

# 型を使って1つ目のたいやきを焼く
taiyaki_1 = Taiyaki()
# 型を使って2つ目のたいやきを焼く
taiyaki_2 = Taiyaki()

と書いたとき、taiyaki_1taiyaki_2class Taiyaki に記述された構造をもとに、メモリ(より厳密には RAM と呼ばれる記憶装置[2])上に別々の記憶領域が割り当てられます。これが「クラスをメモリ上に実体化する」という言葉の意味です。

別々の記憶領域が割り当てられているので、taiyaki_1 に加わった変更は taiyaki_2 や雛形である class Taiyaki には反映されません。たとえば

taiyaki_1 = Taiyaki()
taiyaki_1.nakami = 'つぶあん'

taiyaki_2 = Taiyaki()
taiyaki_2.show()

というコードを実行しても、taiyaki_2.nakami はクラスを定義したときの None が格納されています。

たいやきのたとえは(他の解説に出てくるあらゆるたとえも)雰囲気や取っかかりを理解するにはよいですが、すぐに破綻するのでなるべくあるがままを理解したほうがよいです。たとえば次のコードはたいやきのたとえで説明するのは難しいです。

taiyaki_1 = Taiyaki()
taiyaki_2 = taiyaki_1

taiyaki_1.nakami = 'つぶあん'

# それぞれ何が表示されるでしょうか?
taiyaki_1.show()
taiyaki_2.show()

実行してみるとわかりますが、どちらも 'つぶあん' と表示されます。

上記のコードを理解するには参照(reference)という考え方を理解する必要があります。Python はクラスのように複雑なデータを他の変数に代入するとき、通常は別の記憶領域を確保して中身をコピーすることはせず「メモリ上のどこにあるか」という情報だけをコピーします。以下の

taiyaki_1 = Taiyaki()

というコードは、Taiyaki()がメモリ上の実体を生成して、taiyaki_1にその実体への参照を格納するコードです。

次に

taiyaki_2 = taiyaki_1

というコードは、taiyaki_1 の参照を taiyaki_2 にコピーするコードです。この操作により、taiyaki_2taiyaki_1 と同じインスタンス(メモリ上の記憶領域)を差すようになります。

したがって、こうなった後は taiyaki_1taiyaki_2 のどちらから .nakami にアクセスしても同じ記憶領域が書き換えられることになります。

インスタンスは Taiyaki() を実行するたびに新しく作られるので、

taiyaki_1 = Taiyaki()
taiyaki_2 = Taiyaki()

というコードの場合は taiyaki_1, taiyaki_2 がそれぞれ別々のインスタンスを指すことになり、片方の .nakami を書き換えても、もう片方の .nakami には影響がありません。

参照をコピーするのではなく、中身がまったく同じ値を持つ別のインスタンスを作成したい場合には copy モジュールの deepcopy 関数を使って次のように書きます。

from copy import deepcopy

taiyaki_2 = deepcopy(taiyaki_1)

そろそろたいやきのたとえや図を描かなくても「中身がまったく同じ値を持つ別のインスタンスを作成したい場合には」という言葉の意味を理解し、挙動がイメージできるようになってきたでしょうか?

Python のクラスに関する文法

ここまでの説明がわかった前提で話を進めます。

関数の引数にインスタンスを渡したときの挙動

関数の引数にインスタンスを渡したときは参照がコピーされます。すなわち、

def eat(x):
    x = '食べ終わった'

taiyaki_1 = Taiyaki()
eat(taiyaki_1)
print(taiyaki_1.nakami)

というコードを実行した場合、x には taiyaki_1 が保持している参照がコピーされます。x = '食べ終わった' というコードを実行すると、x の参照先が、taiyaki_1 が参照していたインスタンスから '食べ終わった' という文字列に変わります。このとき taiyaki_1 自体にはなんの影響もないので、taiyaki_1 の参照先が '食べ終わった' に変わることはありません。

一方で、xtaiyaki_1 と同じインスタンスを参照しているので、以下のコードは taiyaki_1.nakami が指しているのと同じ記憶領域を書き換えます。

def eat(x):
    x.nakami = '中身だけ食べた'

taiyaki_1 = Taiyaki()
eat(taiyaki_1)
print(taiyaki_1.nakami)

メンバとメソッド

インスタンスに紐付けられている状態と手続きをそれぞれメンバ(member)、メソッド(method)と言います。class Taiyaki で言えば、nakami がメンバで show がメソッドです。

要するに変数と関数であり、クラスに関わる場合だけメンバとメソッドといちいち厳密に呼び分けるのは面倒臭いので、私の記事では変数、関数と呼んでしまうことがあります(なるべく気をつけますが)。

self 引数

クラスは設計図なので、設計図を書いている段階ではどうやってインスタンスを参照したらよいのか知る術がありません(インスタンスはまだメモリ上に生成されていないからです)。そこで Python では、クラスのメソッドが実行される際にはメソッドの第一引数に、そのメソッドを実行したインスタンスへの参照が渡されるという仕様になっています。この第一引数には慣習的に self という名前をつけることになっています。

class Taiyaki:
    nakami = None
    
    def show(self):
        print(self.nakami)

実行するときにこの self を明示的に記述する必要はありません。

taiyaki_1.show()

流れを確認しておくと taiyaki_1.show() を実行すると、class Taiyakishow メソッドが実行され、このとき self には taiyaki_1 が指しているインスタンスへの参照が格納されます。ゆえに self.nakamitaiyaki_1.nakami と同じ記憶領域を指します。

self 自体は taiyaki_1 が持っている参照のコピーであるため、仮に self = '食べ終わった' のようなコードを書いたとしても taiyaki_1 の参照先自体が変わることはありません。

コンストラクタ

Python のクラスには特殊メソッド(special method)と呼ばれる、特定の役割を持ったメソッドがあります。特殊メソッドのメソッド名はアンダースコア 2 本 __ で挟むという決まりがあります。特殊メソッドの中でももっともよく使うのは、インスタンスが生成されるときに実行されるコンストラクタ(constructor)です。コンストラクタはイニシャライザ(initializer)とも呼ばれ、__init__というメソッド名を持ちます。

class Taiyaki:
    def __init__(self, nakami):
        self.nakami = nakami

    def show(self):
        print(self.nakami)

コンストラクタの引数は Taiyaki() を実行したときの () の中身です。つまり上のようにコンストラクタを定義した場合は、次のように nakami を指定しなければなりません。

taiyaki_1 = Taiyaki('つぶあん')

このコードを実行すると class Taiyaki__init__ メソッドが呼び出され、nakami'つぶあん' が格納されるので、これをそのまま self.nakami にコピーするような挙動になります。

いままでメンバを

class Taiyaki:
    nakami = None

のように定義していましたが、どちらかと言えばメンバはコンストラクタの中で定義するような慣習があります。コンストラクタ以外のメソッド内で新たなメンバが追加される場合があるときも、クラス内にどんなメンバがあるか一覧できないと困るので、コンストラクタ内にすべてのメンバを列挙しておきます(コンストラクタ内でまだ値がわからない場合は = None などにしておきます)。

おしまい

ここまでの内容を理解していればとりあえずクラスの扱いには問題ないので、まずはここまでの文法に慣れてください。体力も尽きたので続きは気が向いたら書きます。

脚注
  1. 厳密にはその中でも手続き型言語と呼ばれるグループ。 ↩︎

  2. RAM は方眼紙のようにびっしりとコンデンサを敷き詰めた構造をしていて、コンデンサに電荷が蓄えられていれば 1、蓄えられていなければ 0 を表すことでデータを記録します。数兆個にもなるコンデンサにはそれぞれ番号が割り振られていて、プログラムは「何番から何番までのコンデンサにこのデータを格納する」という情報を管理しています。 ↩︎

Discussion

ログインするとコメントできます