HOME Python 書き込む

Python のクラスシステム


1. 初めに

この文書の目的は、 Python で自前の class を作るとき、メソッドの第一引数がそのクラスのインスタンスそれ自身 (一般に self と書かれる) であることの理由を説明することです。

他の言語ではメソッドを定義するときに第一引数をインスタンスにするという方法は取り入れておらず、 this などの予約語を使ってインスタンスを表すのが一般的です。 Python だけが、独自の流儀をとっています。 しかし、このことを説明したサイトは見当たりませんでした。 どのサイトもそれは決まりごとだで済ませてしまっています。 しかし、それではどうも Python のクラスシステムを理解した気になれないので、Python でクラスシステムが どのように実装しているか推測しながら、例の self について説明したいと思います。

2. オブジェクト指向プログラミングの復習

オブジェクトとは、データとそれを操作する手続きをまとめたものです。 オブジェクトに属するデータと手続きをまとめてオブジェクトの属性といいます。 オブジェクトは階層構造を持ち、一番下の階層をインスタンスと呼び、 それより上の階層をクラスと呼びます。 クラスも階層構造になっており、下の階層のクラスは上の階層のクラスの属性を受け継ぎます(継承といいます)。 また、必要に応じて上の階層の属性を再定義することができます。 親クラスを複数持つことを許す言語と、親クラスを1つに限定する言語があります。 Python は複数の親クラスを許します(多重継承ありといいます)。

詳しくは、 Wikipedia (日本語) や、 Wikipedia (英語) を見てください。詳しく解説されています。

3. Python のクラスの普通の書き方

まず、普通に Python のクラスシステムを使ってプログラムを書いてみましょう。

IT 企業を自認する 'LiveGate' ではたくさんの IT 技術者を雇っています。技術者たちの情報を管理するプログラムを Python で書きました。多分データベースとつながっていると思いますが、ここではそれは割愛します。 さて、IT 技術者を表すクラスを次の様に書きました。 [code 1] (workers.py)

01:     #! /usr/bin/env python
02:     
03:     """
04:     Workers in a IT company named LiveGate
05:     """
06:     
07:     class Workers:
08:         """ This is a class of workers working in the company."""
09:         
10:         def __init__(self, name, position, email, age, salary):
11:             self.name = name
12:             self.position = position
13:             self.email = email
14:             self.age = age
15:             self.salary = salary
16:             
17:     
18:     class ITWorkers(Workers):
19:         """ This is a class of IT engineers. """
20:         
21:         OS = 'WinNT'
22:         
23:         def __init__(self, language, *av):
24:             Workers.__init__(self, *av)
25:             self.language=language
26:     
27:         def work(self, n):
28:             """ IT engineers should work."""
29:     
30:             if self.position == 'web creator':
31:                 w = 'makes web site'
32:             elif self.position == 'server administrator':
33:                 w = 'checks the trafic'
34:             elif self.position == 'programmer':
35:                 w = 'writes programs'
36:     
37:             print '%s %s for %d, hours using %s on %s' % (self.name, w, n, self.language, self.OS)
38:     
39:     ##------------------------------------------------------------------------------------------------
40:     henley = ITWorkers('PHP', 'Henley', 'web creator', 'henley@livegate.com', 32, 700)
41:     thomas = ITWorkers('Python', 'Thomas', 'server administrator', 'thomas@livegate.com', 37, 900)
42:     gates  = ITWorkers('C', 'Gates', 'programmer', 'gates@livegate.com', 42, 1200)
43:     
44:     henley.OS = 'Mac'
45:     thomas.OS = 'Linux'
46:     
47:     if __name__ == '__main__':
48:     
49:         henley.work(8)
50:         thomas.work(7)
51:         gates.work(10)
まず、労働者を表す class Workers を定義し (7--15 行目)、 そのサブクラスとして ITWorkers を定義しています (18--37 行目)。 18 行目の ITWorkers(Workers)ITWorkersWorkers のサブクラスであることを示しています。 ITWorkers は親クラスの Workers の属性を引き継ぎます。 Workers クラスのインスタンスは初期化されるとき、 インスタンス変数として、名前、職種、e-mail アドレス、年齢、給料が保持されます。 ITWorkers はそれらのほかに使用言語をインスタンス変数として保存します。 使用言語のほかは *av としてレストパラメータに まとめてしまい、Workers.__init__ を呼ぶときそれを展開しています。 ちなみに __init__ はインスタンスを作った直後に呼ばれる特殊メソッドです (省略可能だが、実質的なコンストラクター)。

さて、IT 技術者にも給料分働いてもらわなければならないので、メソッド work を定義します (25--33 行目)。 work の2つ目の引数 n は働いた時間です。 ここでは、職種に応じて何を、何時間したかを表示するだけです。 ついでに、使ったプログラミング言語、OS も表示します。 IT 技術者は当然コンピュータを使うので、その OS が定義されています。 ITWorkers クラスにはクラス変数 OS があり、'WinNT' がデフォルトの OS に設定されています (19 行目)。 つまり、LiveGate では普通は WindowNT を使っているということです。 さて、ここで、3人の IT 技術者に登場してもらいましょう。名前は Henley, Thomas, Gates です。 Henley は web クリエーターです。芸術家肌の彼は Mac を使っています (44 行目)。 Thomas はシステム管理者です。仕事柄、彼は Linux を使います (45 行目)。 プログラマーの Gates はエディターが使えれば OS にはこだわらないのでデフォルトの 'WinNT' を使って 仕事をしています。Henley, Thomaso, Gates は今日それぞれ 8, 7, 10 時間働きました (43--45)。

ここで、注意したいのは、henleythomas に違う OS を設定するということは、 彼らの名前空間に OS というエントリーを加えるということです。gates の名前空間には OS というエントリー がないので、ITWorkers の名前空間まで探しにいきますが、 henleythomas は自分の名前空間に OS が見つかるので わざわざ上に探しに行かないで、自分のを使うということです。 同様に、work というエントリーも個々の IT 技術者の名前空間にはありません。 ITWorkers の名前空間に行って見つけてきます。

workers.py を実行すると次のようになります。

D:\doc\05-07\py_test>python workers.py
Henley makes web site for 8 hours, using PHP on Mac
Thomas checks the trafic for 7 hours, using Python on Linux
Gates writes programs for 10 hours, using C on WinNT

4. もし Python にクラスシステムがなかったら?

ここで、もし Python にクラスシステムがなかったらどうすればよいか考えて見ましょう。 オブジェクト指向を使わないでプログラムを書くことももちろん可能ですが、 ここでは、自力でクラスシステムを作ることを考えます。

実は、関数をデータとして取り扱える言語(広い意味での関数型言語)を用いてオブジェクト指向言語を作るのは 簡単です。各オブジェクトの名前空間はハッシュ表(Python 用語では辞書)を用いて表すことができ、 オブジェクトの階層構造はハッシュ表を階層構造にすることによって表すことができます。 Python も関数をデータとして取り扱えるので、簡単にオブジェクト指向にすることができます。

試しに、workers.py を自前のクラスシステムを使って書き直してみましょう。 書き直したコードを見れば、なぜメソッドの第一引数が self なのかがわかると思います。

[code 2] (workers2.py)

01:     #! /usr/bin/env python
02:     
03:     """
04:     This code demostrates how easy to imprement an object orientated system on a functional programming language.
05:     It only requires a nested hash table.
06:     """
07:     
08:     
09:     def Cls(cls=None, **key):
10:         """ making a new class"""
11:         key['class'] = cls
12:         return key
13:     
14:     def new(cls, **key):
15:         """ making an instance """
16:         key['class'] = cls
17:         return key
18:     
19:     
20:     def geta(obj, attr):
21:         """ getting the attribute of object """
22:         if attr in obj:
23:             return obj[attr]
24:         elif(obj['class']):
25:             return geta(obj['class'], attr)
26:         else:
27:             return None
28:     
29:     def tell(obj, method, *av):
30:         """ tell object do something"""
31:         fun=geta(obj, method)
32:         if callable(fun):
33:             return fun(obj, *av)
34:         
35:     if __name__=='__main__':
36:     
37:         def it_work(self, n):
38:             """This funciton demonstrates how IT engineers work.
39:                Notice that arguments of thie function is identical to the method 'work' in workers.py"""
40:                
41:             if geta(self, 'position') == 'web creator':
42:                 w = 'makes web site'
43:             elif geta(self, 'position') == 'server administrator':
44:                 w = 'checks the trafic'
45:             elif geta(self, 'position') == 'programmer':
46:                 w = 'writes programs'
47:     
48:             print '%s %s for %d, hours using %s on %s' %
                                                 (geta(self, 'name'), w, n, geta(self, 'language'), geta(self, 'OS'))
49:             
50:         workers = Cls() # dummy class
51:         it_workers = Cls(workers, OS='winNT', work=it_work) # class of IT workers
52:     
53:         henley = new(it_workers, language='PHP', name='henley',
54:                                  position='web creator', email='henley@livegate.com', age=32, salary=700)
55:         thomas = new(it_workers, language='Python', name='Thomas',
56:                                  position='server administrator', email='thomas@livegate.com', age=37, salary=900)
57:         gates  = new(it_workers, language='C', name='Gates',
58:                                  position='programmer', email='gates@livegate.com', age=42, salary=1200)
59:         henley['OS'] = 'Mac'
60:         thomas['OS'] = 'Linux'
61:     
62:         tell(henley, 'work', 8)
63:         tell(thomas, 'work', 7)
64:         tell(gates, 'work', 10)
workers2.py では単純化のため、多重継承はなしにします。

まず、クラスを作る関数 Cls とインスタンスを作る関数 new を見てみましょう。 実は、両方ともまったく同じものです。やっていることは、キーワード変数のハッシュ表に親クラスを 表す 'class' を加えて返すだけです。

次に、関数 geta を見てみましょう。これはオブジェクトの属性を探す関数です。 オブジェクトのハッシュ表にその属性がなければ、親クラスのハッシュ表から属性を探すということを 再帰的に行います。これで、継承とオーバーライドが実装できます。 属性がなければ上に探しに行くことで継承が実装できます。また、 下のオブジェクトに属性を定義すれば、上の属性を無視してそれを使うので 再定義が実装できます。

また、関数 tell は オブジェクトに何かをさせる関数です。まず、geta を使ってメソッドを探します。 そして、見つけたメソッドが関数ならば、それを呼んで、結果を返します。

これで、クラスシステムを定義する関数 Cls, new, geta, tell ができました。 きわめて簡単に作れたことに注目してください。

さて、今作ったクラスシステムを使って workers.py を書き直すと 37 行目以降のようになります。

まず、関数 it_work を定義して、IT 技術者の働き振りを表します。 第一引数が self になっていることに注意してください。 関数 it_work の中では geta を使って IT 技術者の属性を 得ています。

そして、it_workers クラスを作るとき、関数 it_work へのポインターを 'work' の値としてます。 つまり、ハッシュ表 it_workers の キー 'work' の値は it_work へのポインターということです。 関数をデータとして扱えると、ハッシュ表に関数を組み込むことができます。

workers.py と同じように三人の IT 技術者を登場させます。 特殊メソッド __init__ をシミュレートするのは簡単ではないので、 ここではインスタンスを作るときに IT 技術者の属性を全てキーワードパラメターとして与えました。 そして、関数 tell を使って3人に働いてもらいます。 workers.py とまったく同じ結果が得られます。

D:\doc\05-07\py_test>python workers2.py
Henley makes web site for 8 hours, using PHP on Mac
Thomas checks the trafic for 7 hours, using Python on Linux
Gates writes programs for 10 hours, using C on winNT
[code 1] と [code 2] を見比べると良く似ていることがわかると思います。
[code 1] [code3]
obj.attribute geta(obj, 'attribute')
obj.method(*av) tell(obj, 'method', *av)
def work(self, n) def it_work(self, n)
これは、偶然ではなく、Python の class は原理的にこのように実装されています。 ( Python リファレンスマニュアル 3. データモデルを見てください。) 実際、geta と同じ働きをする関数 getattr が用意されています。 また、オブジェクトの名前空間を定義するハッシュ表は 特殊変数 __dict__ に定義されています。 試しに、コマンドラインから 'python' とだけ打ち込んで以下のように入力してみてください。 小豆色の文字は応答です。
D:\doc\05-07\py_test>python
Python 2.4.1 (#65, Mar 30 2005, 09:13:57) [MSC v.1310 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
[x]>>> import sys
[x]>>> from workers import *
[1]>>> gates.__dict__
{'salary': 1200, 'name': 'Gates', 'language': 'C', 'age': 42, 'position': 'programmer',
 'email': 'gates@livegate.com'}
[2]>>> henley.__dict__
{'salary': 700, 'name': 'Henley', 'language': 'PHP', 'age': 32, 'position': 'web  creator',
 'OS': 'Mac', 'email': 'henley@livegate.com'}
[3]>>> ITWorkers.__dict__
{'__module__': 'workers', 'work': <function work at 0x00A34630>, 'OS': 'WinNT',
'__doc__': ' This is a class of IT engineers. ', '__init__': <function __init__
at 0x00A345F0>}
[4]>>> ITWorkers.work(gates, 10)
Gates writes programs for 10, hours using C on WinNT
[5]>>>  gates.__class__.work(gates, 10)
Gates writes programs for 10, hours using C on WinNT
[6]>>> getattr(henley, 'OS')
'Mac'
[7]>>> getattr(henley, 'work')
<bound method ITWorkers.work of <workers.ITWorkers instance at 0x00A33760>>
[8]>>> getattr(henley, 'work')(8)
henley makes web site for 8 hours, using PHP on Mac
sysworkers を import してから、 8つのコマンドを入力して遊んでみます。 まず、gates の名前空間を見てみましょう ([1])。いろいろなエントリーがありますが、 'OS' はありません。 次に henley の名前空間を見てみましょう ([2])。 'OS' が定義されています。 さらに、ITWorkers の名前空間を見てみましょう ([3])。 __module__, __doc__ などのあらかじめ定義された変数のほかに 'OS' と work, __init__ が定義されています。 特にメソッドは、<function work at 0x00A34630> のように定義されており、関数としてメモリー上に保持されていることがわかります。 前に述べたように、henley は自分の OS が定義されているので それを使い、gates は定義されていないので、 上に探しに行き、class ITWorkers で見つかった OS を使います。

さて、ITWorkers クラスには work という関数が定義されているので、 [4] のように 直接呼び出して見ましょう。gates.work(10) としたのと同じ結果になります。 また、インスタンスには __class__ という特殊変数があり、 自分が属するクラスを指します。従って、 [5] のように呼び出しても同じ結果が得られます。

最後に getattr を使って遊んでみましょう。[6] に示すように、 getattr(henley, 'OS')henley.OS と同じ結果になります。 次に、メソッドを見てみましょう ([7] )。 次のような結果が返ってきます。
<bound method ITWorkers.work of <workers.ITWorkers instance at 0x00A33760>>
<workers.ITWorkers instance at 0x00A33760> の部分は henley のアドレスのようです。また、この関数は 'function' ではなく 'bound method' と表示されます。 この、'bound method' を [8] のように呼び出すことができます。 このように変化していることが、外から呼び出す場合、第一引数に 自分自身をつけなくて良い理由です。尤も、本当の理由はそんなことするのはクールでないからでしょう。 'bound method' は [code 2] の関数 'tell' の構文糖衣とみなせます。

これらのことから、関数型言語にクラスシステムを導入する場合、 メソッドとして定義される関数はインスタンスを引数にとるのが自然であるといえます。 class のなかで定義される手続きは、 def を使って、通常の関数とまったく同様に定義されていることから、 スコープがそのクラスに限定されたものの、通常の関数と同等であるということができます。 つまり、メソッド定義の第一引数が self であるのは当然の成り行きであるということです。 そうしないと、インスタンスに含まれる変数を参照することができません。

4. 終わりに

Pyhton は基本的には(広い意味での)関数型言語で、オブジェクト指向はハッシュ表を使って後付したものです。 この点、もともとオブジェクト指向言語として設計された C++, Java, Ruby などとは違います。

Python は手続きの定義を、関数定義とメソッド定義に分ける代わりに、 全てを関数定義にし、メソッド定義として使う場合は第一引数をインスタンスに割り当てるという 約束事を導入しました。

このようにしたのは、関数型言語は、オブジェクト指向言語より抽象性が高いという信念でしょう。 Python は関数型言語 Haskell から多大な影響を受けているいわれています。 実際、[code 2] で示したように関数型言語を使えばオブジェクト指向は簡単に実装できます。