Classes

昨日の続きです。

A First Look at Classes

Class Objects

早速クラスを定義してみます。

>>> class MyClass:
...     """A simple example class"""
...     i = 12345
...     def f(self):
...             return 'Hello world'
...


JavaC++などを使っていると、上記の「i」という変数はあたかもインスタンスフィールド、つまりインスタンスに紐付いた変数のように感じてしまいますが、Pythonでは違います。「i」はクラス変数です。つまりインスタンスを生成しなくても参照、更新可能な変数なのです。もちろんfも同様です。

>>> MyClass.i
12345
>>> MyClass.f
<unbound method MyClass.f>


ここで注意が必要なのは「f」です。この関数引数に「self」をとります。名前から察するにこの「self」は自分自身、Javaで言うところの「this」、つまりインスタンスを指します。なので「f」の呼び出しはクラス経由では出来ません。

>>> MyClass.f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method f() must be called with MyClass instance as first argument (got nothing instead)


では引数に何らかのMyClassインスタンスを指定すれば呼び出せるのかな?

>>> x = MyClass()
>>> MyClass.f(x)
'Hello world'


あらら、出来るんですね。あまり一般的な使い方じゃないと思いますが。これを見ると「f」はクラスメソッドであることがよく分かります。


そしてちょっと先走ってしまいましたが、クラスのインスタンスを作る (Install) にはこんな感じで作ります。

>>> x = MyClass()
>>> x
<__main__.MyClass instance at 0xb77bff4c>


もちろんインスタンスを作る際に初期化する場合は専用の初期化関数を定義します。

>>> class Complex:
...     def __init__(self, real, image):
...         self.real = real
...         self.image = image
... 
>>> x = Complex(3.0, -4.5)
>>> x.real, x.image
(3.0, -4.5)


こんな感じで引数を付け加えることも可能です。

Instance Objects

Pythonはとても動的な言語です。インスタンス変数は動的に追加されていきます。たとえばこんな感じ。↑で作成したMyClassクラスを使います。

>>> x.counter = 1    # ここでcounterというインスタンス変数が追加される
>>> x.counter = x.counter *2
>>> x.counter
2


もちろん追加したインスタンス変数を削除することも可能です。

>>> del x.counter
>>> x.counter
Traceback (most recent call last):    # そんなインスタンス変数ないと怒られる
  File "<stdin>", line 1, in <module>
AttributeError: MyClass instance has no attribute 'counter'


まぁ、たいがいはインスタンスメソッド (関数) の中で追加していきます。ということでインスタンスから関数にアクセスしてみましょう。

>>> x.f()
'Hello world'


インスタンスメソッドの定義ではselfという引数が必要でしたが、インスタンスからメソッドを呼び出す時は必要ありません。CとC++が混在するコードを見たことがある人なら馴染みやすいかも知れませんね。ちなみに変数と同じように関数も動的に追加出来るのかなぁ?と思い、実験してみました。

>>> x.g = x.f
>>> x.g()
'Hello world'


行けますね。ちょっと不思議な感じ。

Method Objects

クラスオブジェクト、インスタンスオブジェクトに続いてはメソッドオブジェクト。Pythonではメソッドもオブジェクトなんですね。

>>> x = MyClass()
>>> x.f()
'hello world'
>>> xf = x.f
>>> for i in range(0, 10):
...     print xf()
...
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world


ちょっと不思議な感じがします。MyClassの「f」というメソッドは「self」つまりインスタンス自身を引数にとります。ただし、インスタンスから呼び出す場合は必要ありませんでした。

>>> x.f()    # インスタンスから呼び出す場合は引数必要なし
'hello world'
>>> MyClass.f(x)    # クラスメソッド (関数) として呼び出す場合は引数必要
'hello world'


ですがその上の例では引数は指定していません。その理由は「xf」に代入されたのがクラスメソッド (関数) を表す関数オブジェクトではなく、インスタンスメソッドを表すメソッドオブジェクトだからです。メソッドオブジェクトには引数として渡すインスタンスそのものも含んでいるわけです (コンテキストを含む、というと分かりやすい?) 。でも大元は関数オブジェクトです。メソッドオブジェクトは関数オブジェクトをラップしているのです。

Random Remarks

ここではちょっとした注意事項がいくつか書かれていました。まず一つ目はPythonではメソッドを変数でオーバーライド出来る点について。なのでメソッド、変数の名前を付ける時は決まりを作りましょう。。。う〜ん、あんまり解決になっていないような。。。次にPythonでは基本的にはメソッドも変数も全てpublicである点について。こちらは注意せよ。。。ってこちらもあんまり解決になってないような気がします。いろいろしきたりみたいなものがありそうですね。

Inheritance

さてこの辺りから面白くなってきます。やっぱりこれがないとオブジェクト指向言語とは言えませんよね。App Engineのチュートリアルをやっているとよく出てくる表現です。こんな感じです。

>>> class Base:
...     """This is a simple base class."""
...     def f(self):
...         print "Base::f()"
...     def g(self):
...         print "Base::g()"
... 
>>> class Derived(Base):
...     """This is a simple derived class."""
...     def f(self):
...         print "Derived::f()"


もちろんBaseクラスから継承してDerivedクラスが定義されています。インスタンスを生成 (Installation) してみましょう。

>>> x = Derived()
>>> x
<__main__.Derived instance at 0xb77f8d0c>


まぁ、普通のクラスと変わりませんね。メソッドを呼び出してみましょう。

>>> x.f()
Derived::f()
>>> x.g()
Base::g()


予想どおりですね。Pythonはベースクラスと同じ名前の関数を定義すると必ず上書きされます。この辺り、ちょっとSmalltalkっぽい感じがします。Javaのfinal指定 (サブクラスでオーバーライドさせない) ような指定はPythonにはなさそうですね。ちなみにオーバライドされたベースクラスのメソッドを呼びたければ以下のようにします。

>>> Base.f(x)
Base::f()


メソッドとしてではなく、関数として呼び出してしまいます。こうなるとオーバーライドもへったくれもなくなります。必要だからオーバーライドしているわけで、それを無視してまでベースクラスのメソッドを呼ぶのはどうかな、と思います。またインスタンスとクラスの関係やクラス間の関係 (メタ情報) をチェックする機能があります。

>>> isinstance(x, Derived)    # xはDerivedクラスのインスタンス?
True
>>> isinstance(x, Base)       # xはBaseクラスのインスタンス?
True
>>> isinstance(x, int)        # xはintクラスのインスタンス?
False
>>> issubclass(Derived, Base) # DerivedクラスはBaseクラスのサブクラス?
True
>>> issubclass(Base, Derived) # BaseクラスはDerivedクラスのサブクラス?
False
Multiple Inheritance

なんとPythonでは多重継承が可能です。Javaに慣れている私にはちょっと馴染めない概念です。でもモデリングという観点で言えば多重継承は全然アリだと私は思います。


閑話休題Pythonで多重継承を表現するにはこんな感じで書きます。

>>> class Base1:
...     def f(self):
...         print "Base1.f()"
... 
>>> class Base2:
...     def f(self):
...         print "Base2.f()"
... 
>>> class Base3:
...     def f(self):
...         print "Base3.f()"
... 
>>> class Derived(Base1, Base2, Base3):
...     pass


多重継承を論じるときに必ず問題になるのが、メソッドや変数の検索順序。Pythonでは深さ優先で検索します。↑の例ではDerivedクラスのベースクラスが、Base1, Base2, Base3の順で記述されています。この記述の順番で一番上位のベースクラスまで検索されます。それが終わっても見つからなければ次はBase2で始まるクラス階層を検索する、というわけです。なので、上の例でいれば、Derivedクラスの継承順序をたとえば

class Derived2(Base3, Base2, Base1):


とすれば、Base3クラスから検索が始まります。なお、Pythonではこの検索順序をプログラムの中から変えることも可能らしいのですが、混乱しそうなので止めておきます。

Private Variables

PythonにはPrivateな変数やメソッド、関数はありません。が、Privateのように振る舞わせることが出来ます。その手法のことをManglingといいます。たとえばクラス内で

>>> class Mangling:
...     def __test(self):
...         print "Mangling.__test()"


というようにアンダースコア「_」を二つ付けたメソッドを定義したとします。これを呼び出してみようとすると

>>> x = Mangling()
>>> x.__test()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: Mangling instance has no attribute '__test'


そんなメソッド (属性) ないよと怒られます。実はアンダースコアを二つメソッド名や変数名の先頭に付けるとインタープリターが「_classname__」というように前にアンダースコア + クラス名を入れてくれます。なので

>>> x._Mangling__test()
Mangling.__test()


というように呼び出すことは出来ます。でも呼び出せないに等しいですよね。擬似的ですが、ちゃんとPrivateの動きをしてくれます。ここで一番重要なのはサブクラスでオーバーライドされない、ということです。

>>> class DerivedMangling(Mangling):
...     def __test(self):
...         print "DerivedMangling.__test()"


と定義するとベースクラスの__test(self)メソッドがオーバーライドされてしまいそうですが、実際はされません。Manglingされますから。

>>> z._Mangling__test()
Mangling.__test()
>>> z._DerivedMangling__test()
DerivedMangling.__test()


名前が違うから当然と言えば当然ですね。

Odds and Ends

C言語の構造体のように変数だけをメンバーに持つデータ構造を表すにはこんな感じに動的にフィールドを追加してやるやり方が便利です。

>>> class Employee:
...     pass
...
>>> john = Employee()
>>> john.name = 'John Doe'
>>> john.dept = 'computer lab'
>>> john.salary = 1000
>>> print john.name, john.dept, john.salary
John Doe computer lab 1000


う〜ん、私ならこうしますね。

...     def __init__(self, name, dept, salary):
...             self.name = name
...             self.dept = dept
...             self.salary = salary
... 
>>> john = Employee2('Kensuke Nakai', 'Development', 100)
>>> print john.name, john.dept, john.salary
Kensuke Nakai Development 100


動的にメンバーを増やすのに慣れていないので。

Exceptions Are Classes Too

以前、例外クラスを定義しましたが、実はPythonはExceptionクラスのサブクラスでなくても例外をあげることが可能です。

>>> class B:
...     pass
... 
>>> class C(B):
...     pass
... 
>>> class D(C):
...     pass
... 
>>> for c in [B, C, D]:
...     try:
...         raise c()
...     except D:
...         print "D"
...     except C:
...         print "C"
...     except B:
...         print "B"
... 
B
C
D


Exceptionクラスのサブクラス以外を例外として投げるなんて思いもよりませんでした。ちょっと面白いですね。

Iterators

これまでにリストやタプル、ファイルなんかをfor文で使って来ました。

>>> for element in [1, 2, 3]:
...     print element,
... 
1 2 3
>>> for element in (1, 2, 3):
...     print element,
... 
1 2 3
>>> for line in open('test.txt','r'):
...     print line,
... 
This is the first line of file.
Second line of the file.
This is a test.


この仕組みですが、for文を使わずに再現させると理解出来ます。

>>> a = [1, 2, 3]
>>> iterator = iter(a)
>>> iterator
<listiterator object at 0xb77fb90c>
>>> iterator.next()
1
>>> iterator.next()
2
>>> iterator.next()
3
>>> iterator.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration


つまり、iter関数でイテレータオブジェクトを取得し、後は例外 (StopIteration) が発生するまでnextメソッドで中の要素を取り出していきます。この動作がfor文で行われています。となると自分でfor文にかけられるクラスを作ってみたくなりますよね。

>>> class Reverse:
...     def __init__(self, data):
...             self.index = len(data)
...             self.data = data
...     def __iter__(self):
...             return self
...     def next(self):
...             if self.index == 0:
...                     raise StopIteration
...             self.index = self.index - 1
...             return self.data[self.index]
...
>>> rev = Reverse('spam')
>>> for char in rev:
...     print char
... 
m
a
p
s


iter関数はインスタンスの__iter__メソッドを呼びます。for文はこの戻り値として渡されるインスタンスのnextメソッドを例外が発生するまで呼びつづけるわけです。