本記事はPython公式チュートリアル9章クラスの9.3., 9.4., 9.5.節の内容をPython初心者の私なりにまとめなおしたものです。今後の理解度の進展に応じて随時更新します。
この記事の対象者
- Pythonクラスについて知りたい方
- Pythonクラスを使いこなしたい方
- Pythonクラスのインスタンス名前空間にメソッドがあると思っている方(過去の私)
0 概要
クラス(class)とは、データと機能をひとまとめにラップしたオブジェクトのことです。ここではデータとは、オブジェクトを束縛する変数、機能とは関数のことを指しています。今回取り上げたいのは、次の3つのテーマです。
- クラスの基礎
- クラスの継承
- クラスと名前空間
特に、「3. クラスと名前空間」ではPythonのクラス定義中に頻繁に登場する"self"の役割について明確に言及します。本記事では、名前空間の知識を前提としています。気になる方は、筆者の前記事 Python探求1(名前空間)をご参照ください。
1 クラスの基礎
この節ではわかりやすさを重視し、名前空間を厳密には扱いません。モヤモヤを感じた場合、3節を参照してください。Pythonではクラス定義に入ると、新たな名前空間が作成され、ローカルな名前空間として扱われます。従って、ローカルな変数に対する全ての代入はこの新たな名前空間に入ります。特に、関数定義を行うと、新たな関数の名前はこの名前空間に結び付けられます。Pythonにおいて、名前空間は辞書型で定義されるのでした。実際にクラスの名前空間を確認するには、__dict__属性を参照します。
定義(クラスオブジェクト) クラスオブジェクトとは、クラス定義で作成されたオブジェクトのこと。
class MyClass:
# ローカル空間
"""A simple example class"""
i = 12345 # クラス変数
def __init__(self, name):
self.name = name # インスタンス変数
def f(self):
return 'hello world'
上のコードでは、MyClassという新たなクラスオブジェクトを定義しています。クラスオブジェクトは2種類の演算を持ちます。それは、属性参照(attribute reference)とインスタンス作成(instantiation)です。属性参照とは、MyClass.i, MyClass.f のような構文でクラスに属する変数、関数を呼び出す演算のことです。インスタンス作成は、意外と奥が深いですが作り方は次のような関数記法に従います:
x = MyClass('Python')
インスタンス化では、まず空のオブジェクトが作成されます(これは文字通り空っぽで、インスタンスの名前空間は空の辞書です)。しかし、特定の初期状態を持っていてほしいことが多いので、__init__関数で情報を持たせます。上の例では、name に Python という情報を持たせています。__init__関数はインスタンス化の際に空のオブジェクトが作成された直後に自動的に呼び出されます。名前空間の言葉でいえば、インスタンス x がもつローカル空間は {'name': 'Python'} になります。
定義(インスタンスオブジェクト) インスタンスオブジェクトとは、インスタンス作成で作成されたオブジェクトのこと。
インスタンスオブジェクトは、クラスオブジェクトとは真に異なるローカル空間を生成します。また、インスタンスオブジェクトはただ一つの演算しか持ちません。属性参照です。有効な属性はデータ属性とメソッドの2種類です。インスタンスオブジェクトで理解に苦労するのは、関数オブジェクトとメソッドオブジェクトの違いについて理解することです。MyClass.f と x.f は厳密に異なり、次が成り立ちます。
x.f() == MyClass.f(x)
クラス内の関数は第一引数に"self"なる変数を持っていました。実はこれは、インスタンスオブジェクトのことです(別に"self"でなくても名前は何でもよい)。誰しも一度は、「"self"って何?」という疑問を抱くと思いますが、これはインスタンスオブジェクトのローカル空間にアクセスするための引数であるということがこれでわかったと思います。詳しくはまた3節で言及するので、とりあえず今はスルーしても問題ないです。ただし、"self"はただの"おまじない"ではないことは肝に銘じましょう。
最後に、クラス変数とインスタンス変数の違いについて述べます。クラス変数は、クラスの名前空間に属する変数です。一方、インスタンス変数は各インスタンスの名前空間に属する変数です。一つ注意点があります。クラス変数にミュータブルな変数を使うのは一般的に推奨されません。なぜなら、あるインスタンスで変更を加えた場合に、他のインスタンスでもその変更が共有されてしまうからです。
2 クラスの継承
Pythonにおいて、クラスの継承は次の構文で実行します。
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
継承クラスオブジェクトが構築される時、基底クラス(継承元のクラス)が記憶されます。記憶された基底クラスは、属性参照を解決するために使われます。要求された属性がクラスに見つからなかった場合、基底クラスに検索が進みます。この規則は、基底クラスが他の何らかのクラスから派生したものであった場合、再帰的に適用されます。
派生クラスのインスタンス化では、特別なことは何もありません。 DerivedClassName() はクラスの新たなインスタンスを生成します。メソッドの参照は次のようにして解決されます。まず対応するクラス属性が検索されます。検索は、必要に応じ、基底クラス連鎖を下って行われ、検索の結果として何らかの関数オブジェクトがもたらされた場合、メソッド参照は有効なものとなります。
ここまでは、特に何も難しいことはありません。問題は、2つ以上の基底クラスを持つ多重継承クラスで起こります。
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
ほとんどのシンプルな多重継承において、親クラスから継承される属性の検索は、深さ優先で、左から右に、行われます。なので、ある属性が DerivedClassName で見つからない場合、まず Base1 から検索され、そして(再帰的に) Base1 の基底クラスから検索され、それでも見つからなかった場合は Base2 から検索される、といった具合になります。
しかし、多重継承クラスでは、しばしば共通の基底クラスを持つ親クラスが複数存在する場合があります。このとき、基底クラスが複数回アクセスされないように、メソッドの検索順序を動的に線形化することがあります。これを Method Resolution Order (MRO) といいます。例を見てみましょう。
class A:
def method(self):
print("A's method")
class B(A):
def method(self):
print("B's method")
class C(A):
def method(self):
print("C's method")
class D(B, C):
pass
d = D()
d.method()
上の例では、MROに従って、"B's method"がプリントされます。参照順序は、D→B→A→Cです。より複雑なクラス構成の場合に MRO は威力を発揮します。しかし、本記事の主眼とややずれるので詳しくは別の機会にしましょう(参考:Python公式 MRO)。
3 クラスと名前空間
Pythonの属性解決メカニズムは、インスタンスオブジェクト⇒クラス(⇒基底クラス)という順番で基本的に進行します。多くの場合インスタンスオブジェクトの名前空間は、__init__関数内で定義された独自の変数名を含むのみです。メソッドの参照は、MROに従います。クラス内に定義された関数は、第一引数に"self"を持ちます。これは、インスタンスオブジェクトの名前空間内にある__init__の変数または継承元の属性にアクセスするためです。それ以上でも以下でもありません。さて、今は次の等式がとても自然に思えてきたのではないでしょうか。
x.f() == MyClass.f(x)
繰り返しますが、多くの場合インスタンスオブジェクトの名前空間は、__init__関数内で定義された独自の変数名を含むのみです。クラスで定義された関数は含みません。インスタンス x がメソッド f を使えるのは、 Pythonの属性解決メカニズムのおかげです。メソッドオブジェクトとは、インスタンスオブジェクトとクラスオブジェクトを参照し、メソッド引数リストの先頭にインスタンスオブジェクトを追加した新たな引数リストを(存在する場合)関数オブジェクトに代入するものです。このような仕組みのおかげで、メモリを節約しつつインスタンス変数をメソッド内で使えるわけです。
class MyClass:
# ローカル空間
"""A simple example class"""
i = 12345 # クラス変数
def __init__(self, name):
self.name = name # インスタンス変数
def f(self):
return 'hello world'
更に、上の__init__関数の記述も今では自然であるとしか思えないことでしょう。単に、インスタンスオブジェクトの名前空間に変数を追加しているだけです。
4 まとめ
以上3節で、Pythonのクラスと名前空間についての説明を終えます。私はそもそもインスタンスオブジェクトのことを、単にクラスのコピー的なものだと思っていたので、理解に時間がかかってしまいました。しかし、メモリ管理を考えるとそんな非合理的なことをするはずがありませんね。。。
次回予告
データ型、特にイテレーター型についてまとめる予定です。
ご閲覧いただき、ありがとうございました!