[Python入門]クラスのスコープとプライベートな属性:Python入門(1/2 ページ)
クラスと名前空間やスコープの関係について見た後に、外部には見せたくない「プライベートな属性」の扱いについて見てみよう。
前回はリストを継承してスタックを作りながら、その意義とリストを内部で所有したスタックとの違いなどについて見た。今回は少し話題を変えて、クラスとスコープや名前空間、それからプライベートな属性の扱いについて見ていこう。
クラスと名前空間とスコープ
クラスの定義を名前空間という観点から考えてみよう。詳細についてはPythonのドキュメント「クラス定義の構文」を参照してほしい。ここでは、次のようなクラスを定義する。
class Foo:
def hello(self):
print('Hello from Foo')
def some_method(self):
local_var = 'local var in hello method'
print('local namespace:', dir())
print('global namespace:', globals().keys())
class文によりクラス定義が開始されると、名前空間が新しく作成される。そして、クラス定義内で行われた代入やメソッド定義により、新しく名前が作られると、それはその名前空間に記憶される。つまり、上のコードなら「hello」と「some_method」という2つの名前はFooクラス用に作られた名前空間に保持され、Fooクラスの属性となる。そして、クラス定義が終了すると、そのクラスを表すオブジェクトが作成され、その名前空間の内容(と基底クラスで定義されている属性)がそのオブジェクトに保持される。同時に、そのクラスの名前がクラスを定義していたローカルの名前空間に追加される(そのため、クラス定義が終わると、すぐにそのクラスを利用できる)。
ちなみにhelloメソッドは単に「Hello from Foo」というメッセージを表示するだけのもので、some_methodメソッドはそのメソッドのローカルスコープとグローバルスコープに存在する名前を表示するものだ。
このクラスの動作を確認してみよう。
foo = Foo()
foo.some_method()
このコードを実行すると、次のようになる。
some_methodメソッドのローカルスコープにはパラメーターの「self」とローカル変数の「local_var」だけが存在する。一方、グローバルスコープには、Pythonの実行環境が自動的にセットアップするもの(__name__、__dict__、In、Out、_、__、_i1など多数)を除けば、上で定義しているクラスの名前である「Foo」とそれを利用して作成したインスタンスの「foo」だけが存在している。
このことから分かるように、クラスで定義した各種メソッドは、そのインスタンスメソッドから直接(インスタンス変数名またはメソッド名と引数だけを指定するだけ)アクセスできるわけではない。
では、それらにどのようにアクセスするかは既にお分かりだろう。メソッドに渡されるパラメーター「self」を介してアクセスすればよい。クラスのインスタンスを生成すると、そのオブジェクトの属性として、上で述べたクラスの名前空間に含まれている名前(メソッド名やクラス変数名)が引き渡される。実際に確認してみよう。
class Foo:
def hello(self):
print('Hello from Foo')
def some_method(self):
print('self namespace:', dir(self))
print('Foo namespace:', dir(Foo))
self.hello()
ここでは、some_methodメソッドを修正し、selfが持つ属性とクラスが持つ属性を調べて、最後にhelloメソッドを呼び出すようにした。これを先ほどと同様のコードで確認してみよう。ただし、今度はそのインスタンスに属性(インスタンス変数)を追加している。
foo = Foo()
foo.some_value = 'some value'
foo.some_method()
これらのコードを実行すると、次のようになる。
出力が多くて少し分かりにくいかもしれないが、「dir(self)」の出力結果には、インスタンス固有の属性「some_value」があるが(赤枠内)、「dir(Foo)」の出力結果にはこれが含まれていないことに注目しよう(Fooクラスとそのインスタンスでは属性の数が多くないのに、これほど多くの属性が表示されるのは、Fooクラスの基底クラスであるobjectクラスの属性を引き継いでいるからだ)。クラス定義により、そのクラスにはそこで定義された名前(と基底クラスで定義されている名前)が属性として追加され、インスタンスを生成すると、それらがインスタンスの属性にもなる。加えて、__init__メソッドやその他の方法によりインスタンスに属性が追加されると、それらはインスタンスが個々に持つ属性になるということだ。
クラスのインスタンスメソッドでは、今見たように主に「ローカルスコープ」「グローバルスコープ」「組み込みスコープ」に加えて「selfを介してアクセスできる属性(クラス定義で記述されたものと、インスタンスごとに固有の属性)」が利用可能となる。
クラスメソッドでは、第1パラメーターのclsにはクラス自身が渡されるので、インスタンスごとに固有の属性にはアクセスできないが、クラスの名前空間に追加された名前にはアクセスできる。よって、「ローカルスコープ」「グローバルスコープ」「組み込みスコープ」「clsを介してアクセスできる属性(クラス定義で記述されたもの)」が利用できるということだ(インスタンスメソッドとして定義したメソッドも含まれるが、その場合は「cls.インスタンスメソッド(処理対象, 引数)」という呼び出しをする必要がある。つまり、メソッドではなく、関数として呼び出すことになる)。
クラスの継承と名前空間とスコープ
では、クラスを継承するとどうなるだろうか。ここでは以下のような基底クラス(Foo)と派生クラス(Bar)を定義してみよう。ここではそれぞれのクラスでhelloメソッドを定義して(BarクラスではFooクラスのhelloメソッドをオーバーライド)、それ以外に上で見たのと同様なインスタンスの属性を調べるメソッドと、helloメソッドも呼び出して「ハロー」「グッバイ」と表示するメソッドを定義している。
class Foo:
def hello(self):
print('Hello from Foo')
def hello_goodbye(self):
self.hello()
print('Goodbye from Foo')
def show_attr(self):
print(f'{self.__class__}: {dir(self)}')
class Bar(Foo):
def hello(self):
print('Hello from Bar')
def goodbye(self):
print('Googbye from Bar')
これにより、2つのクラスとそれらに固有の名前空間が作成される。Fooには2つの名前「hello」と「hello_goodbye」(とobjectクラスから受け継いだ属性)が、Barには「hello」(とその基底クラスであるFooクラス、さらにその基底クラスであるobjectクラスから受け継いだもの)が属性として登録される。というのは、Fooクラスで見たのと同様だ。
なお、show_attrメソッドではf文字列内で「self.__class__」という記述をしているが、これはクラス名を取得するためのものだ(「__class__」はそのオブジェクトが何の型に属すものなのかを示す「特殊属性」と呼ばれる属性)。
これらのクラスの動作を先ほどと同様なコードで確認してみよう。
foo = Foo()
foo.show_attr()
bar = Bar()
bar.show_attr()
これを実行すると次のようになる。
実行結果を見ると、Fooクラスのインスタンスでは基底クラス(objectクラス)から受け継いだものに加えて3つのインスタンスメソッドの名前があることが分かる(上の赤枠内)。Barクラスのインスタンスでは基底クラス(Fooクラス)から受け継いだものに加えて、このクラスで独自に定義したgoogbyeメソッドがあることが分かる(下の赤枠内)。
注意したいのは、Barクラスでオーバーライドしているhelloメソッドだ。上の実行結果では、Fooクラスのインスタンスが持つ属性の表示にも、Barクラスのインスタンスが持つ属性の表示にも「hello」という名前が表示されているが、これらが実際に別々のインスタンスメソッドを参照するものであることは既にお分かりだろう。以下のコードを試してみよう。
foo.hello()
bar.hello()
ここでは先ほど作成したFooクラスのインスタンスとBarクラスのインスタンスに対してhelloメソッドを実行している。その実行結果を以下に示す。
当然のように、両者のクラスで定義されているhelloメソッドが呼び出されている。このことから分かるのは派生クラスで定義した名前により、基底クラスで定義した名前が上書きされる(オーバーライドされる)ことだ。これを図にすると次のようになる。クラスで定義されている名前(メソッドやインスタンス変数)を利用するときには、概念的には「派生クラスで定義されているかどうか」がまずは検索され、なければ「基底クラスで定義されているかどうか」が検索される。これが最終的に全ての基底クラスであるobjectクラスにたどり着くまで繰り返されて、その連鎖の中で指定した名前が見つからなければエラー(NameError例外)となる。
その一方で、Barクラスでオーバーライドしていないメソッドを呼び出せば、Fooクラスで定義したものが呼び出される。
foo.hello_goodbye()
bar.hello_goodbye()
このコードを実行すると次のような結果になる。
hello_goodbyeメソッド内では「self.hello()」としてhelloメソッドを呼び出しているが、selfが参照しているのがFooクラスのインスタンスか、Barクラスのインスタンスかで実際に呼び出されるメソッドが変わっている(メッセージが「Hello from Foo」と「Hello from Bar」になっている)点にも注意しよう。
Copyright© Digital Advantage Corp. All Rights Reserved.