メソッドオブジェクト(3)

【 目次 】

メソッド内からの変数へのアクセス

ここでメソッド内からの変数へのアクセスについて整理してみよう。

スコープと名前空間

まず、以下の公式ドキュメントを参照して、pythonのスコープと名前空間について考えてみよう。

  • 9.2. Python のスコープと名前空間 - 9. クラス — Python 2.7ja1 documentation

    インタプリタのトップレベルで実行された文は、スクリプトファイルから読み出されたものでも対話的に読み出されたものでも、 __main__ という名前のモジュールの一部分であるとみなされるので、独自の名前空間を持つことになります。 (組み込みの名前は実際にはモジュール内に存在します。そのモジュールは __builtin__ と呼ばれています。)
    ...
    インタプリタのトップレベルで実行された文は、スクリプトファイルから読み出されたものでも対話的に読み出されたものでも、 __main__ という名前のモジュールの一部分であるとみなされるので、独自の名前空間を持つことになります。 (組み込みの名前は実際にはモジュール内に存在します。そのモジュールは __builtin__ と呼ばれています。) スコープ (scope) とは、ある名前空間が直接アクセスできるような、 Python プログラムのテキスト上の領域です。 “直接アクセス可能” とは、修飾なしに (訳注: spam.egg ではなく単に egg のように) 名前を参照した際に、その名前空間から名前を見つけようと試みることを意味します。

    スコープは静的に決定されますが、動的に使用されます。実行中はいつでも、直接名前空間にアクセス可能な、少なくとも三つの入れ子になったスコープがあります。

    • 最初に探される、最も内側のスコープは、ローカルな名前を持っています。
    • 外側の(enclosing)関数のスコープは、近いほうから順に探され、ローカルでもグローバルでもない名前を持っています。
    • 次のスコープは、現在のモジュールのグローバルな名前を持っています。
    • 一番外側の(最後に検索される)スコープはビルトイン名を持っています。
    ...
    通常、ローカルスコープは (プログラムテキスト上の) 現在の関数のローカルな名前を参照します。関数の外側では、ローカルスコープはグローバルな名前空間と同じ名前空間、モジュールの名前空間を参照します。クラス定義では、ローカルスコープの中にもう一つ名前空間が置かれます。

これをふまえて、メソッド内の変数へのアクセスについて考えてみる。

グローバル変数のアクセス

g_var=u"グローバル変数"

class MyCls:
    c_var = u"クラス変数"

    def i_method(self):
        print g_var

my_inst=MyCls()
my_inst.i_method()

関数と同様メソッド内からメソッドの外側のグローバル変数のアクセス(読み込み)は可能である。
また、関数と同様グローバル変数に値を代入するにはglobal宣言が必要である。
そうしないとグローバル変数と同名のローカル変数に代入をおこなった事になってしまう。

インスタンスメソッドからのクラス変数やインスタンス変数へのアクセス

では、メソッドの外側のクラス内に定義されている変数(すなわちクラス変数)にアクセスするには。

上記コードのi_methodメソッドを修正した、 以下のコードはエラーになってしまう。

    def i_method(self):
        print c_var

何も修飾子を付けずに変数に直接アクセスするとそれはローカル変数かグローバル変数へアクセスしたことになってしまう。

NameError: global name 'g_var' is not defined

関数の場合は通常、関数の外側の変数にそのままアクセスできるが、メソッドの場合にはメソッドの外側のクラス内に定義されている変数には変数名だけで直接アクセスする事はできない。

メソッド内からクラス変数にアクセスするには、
インスタンス変数と同様にself.を付加して、

    def i_method(self):
        print self.c_var

もしくはクラス名を直接指定して

    def i_method(self):
        print MyCls.c_var

但し、self.を付加する方法は、同名のインスタンス変数が存在した場合、
インスタンス変数がクラス変数を隠してしまうので、クラス名を直接指定してアクセスする事になる。

しかし、他に方法が無いのかと言うと、インスタンスオブジェクトの持つ自分自身のクラスを示す__class__属性を利用してアクセスする事もできる。

    def i_method(self):
        self.c_var=u"インスタンス変数"
        print self.c_var            # インスタンス変数c_varへのアクセス
        print self.__class__.c_var  # クラス変数c_varへのアクセス

ここで、ユーザ定義メソッドの特殊属性の存在を思い出してみよう。
変則的な方法であるが、上記のコードの最後の行を以下のように特殊属性im_classを使って置き換える事ができる。

        print self.i_method.im_class.c_var

コードが長くなってしまうのでここでは良い方法とは言えないが、困った時にはユーザ定義メソッドの特殊属性を思い出してみるのも役に立つ時があるかもしれない。

クラスメソッドやスタティックメソッドからのクラス変数へのアクセス

クラスメソッドの場合はインスタンスを示す引数selfが使えないので、代わりに引数clsを使ってクラスの属性にアクセスする。

クラスメソッドやスタティックメソッドはインスタンスメソッドとは異なり、特定のインスタンスに付随するメソッドでは無くクラスのメソッドなので、インスタンスの属性にアクセスする事はできない。

スタティックメソッドでは変数selfやclsが使えない。
ではスタティックメソッドでは自分自身の属するクラスの属性にどうやってアクセスするかというと、自分自身のクラスを直接指定して静的にアクセスするしか方法が無いようだ。

    @staticmethod
    def s_method(self):
        print MyCls.c_var

関数をメソッドに動的にバインドする。

インスタンスメソッドをバインド

通常の関数をインスタンスメソッドとして使う事ができる。

インスタンスメソッドとして使うには引数が必要なので引数を1つ用意して、
まずは、普通に関数を定義して呼び出す。

def func(arg_object):
    print arg_object.__class__

func(object)

実行結果

<type 'type'>

この関数funcをMyClsのmethodという属性と結び付けて、これをインスタンスメソッドとして実行。

class MyCls(object):
    method = func

my_inst = MyCls()
my_inst.method()
<class '__main__.MyCls'>

クラスを定義した後で関数をメソッドにバインドする事もできる。

class MyCls(object):
    pass

MyCls.method = func

my_inst = MyCls()
my_inst.method()

他にも、MethodTypeを使う方法もある。

import types
MyCls.method = types.MethodType(func, None, MyCls)

インスタンスメソッドの引数selfの値に他のクラスのインスタンスを渡してあげることはできるのか?

インスタンスメソッドはクラスに属するメソッドオブジェクトであるので、以下のようにクラスオブジェクトよりクラスのインスタンスを引数にして呼び出す事ができる。

class MyCls(object):

    def method(self):
        print u"クラスの名前は", self.__class__

my_inst = MyCls()
MyCls.method(my_inst)

実行結果

クラスの名前は `<class '__main__.MyCls'>`

では、他のクラスのインスタンスを引数selfに指定して呼び出す事はできるのだろうか?

class OtherClass(object):
    pass

other_class = OtherClass()
MyCls.method(other_class)

実行結果

TypeError: unbound method method() must be called with MyCls instance as first
 argument (got OtherClass instance instead)

型のチェックがされているようでエラーになってしまった。
このようなイレギュラーな呼び出しは禁止されているのだろう。

しかし、継承関係にあるクラスのインスタンスの場合には許されているようだ。

class MyClsEx(MyCls):
    pass

my_inst_ex = MyClsEx()
MyCls.method(my_inst_ex)
クラスの名前は `<class '__main__.MyClsEx'>`

ここで、ユーザ定義メソッドの特殊メソッドim_funcの存在を思い出してみよう。
im_funcを使えばメソッドのもとになっている関数を取り出す事ができる。

im_funcを使えば継承関係にないクラスのインスタンスを引数にして呼び出す事ができる。

MyCls.method.im_func(other_class)
クラスの名前は <class '__main__.OtherClass'>

クラスメソッドとスタティックメソッドをバインド

classmethodstaticmethodという組込み関数が存在する。
この関数はどうやって使うのだろうと考えあぐねて、ひょっとしてクラスメソッドとスティックメソッドをバインドのためかな? (って言うかデコレータなら当然か。)

と思って試してみたのが以下のコード

def func(cls):
    print cls.__class__

MyCls.c_method2=classmethod(func)
MyCls.c_method2()

実行結果

<type 'type'>

インスタンス経由でも実行可能。

my_inst.c_method2()

print文でメソッドの特殊属性を確認。

print MyCls.c_method2
print MyCls.c_method2.im_self
print MyCls.c_method2.im_class

実行結果

<bound method type.func of <class '__main__.MyCls'>>
<class '__main__.MyCls'>
<type 'type'>

クラスメソッドとして認識されているっぽい結果が。

スタティックメソッドについても確認

MyCls.s_method2=staticmethod(func)
MyCls.s_method2("abc")      # クラス経由で実行
my_inst.s_method2("abc")    # インスタンス経由で実行

print文で確認。

print MyCls.s_method2
<function func at 0x023893F0>

これもスタティックメソッドとして認識されているっぽい。

メソッドを関数として実行

im_func属性を使えば、逆にメソッドからそのもっとなる関数部分を取り出して、関数として実行させる事ができる。

通常、引数selfの無い関数(インスタンスメソッド)はメソッドとして実行するとエラーになってしまうがim_funcを使って関数として実行させる事ができる。

class MyCls(object):
    def func():
        print "selfの無いメソッドを実効"

MyCls.func.im_func()

特異メソッドとMethodTypeによるインスタンスメソッドの動的生成

rubyでは特異メソッドというメソッドが定義できる。

インスタンスメソッドはクラスの属性であり、同じクラスのインスタンスであれば同じインスタンスメソッドが使える。
特異メソッドはこれに反して、特定のインスタンスのみで使えるインスタンスメソッドの事を指す。

pythonでも特異メソッドは定義できるのだろうか?

特定のインスタンスでのみ使えるメソッドだからと言って以下のように単純に関数をインスタンスにバインドしてもダメ。

class MyCls(object):
    pass

def func(self):
    print self.__class__.__name__

my_inst=MyCls()
my_inst.method=func

上記のコードは、単にインスタンスの属性に関数をバインドしただけなので、関数として実行する事はできるが、引数selfが渡されないのでインスタンスメソッドでは無くなってしまう。

my_inst.method()

実行結果

Traceback (most recent call last):
  File "ソースファイルのパス", line xx, in <module>
    my_inst.method()
TypeError: func() takes exactly 1 argument (0 given)

メソッドとして実行するとエラーが発生。

ではあらためて、
特異メソッドのサンプルコードを

class MyCls(object):
    pass

def func(self):
    print self.__class__.__name__

import types

my_inst=MyCls()
my_inst.method = types.MethodType(func, my_inst)
my_inst.method()

実行結果

MyCls

メソッドがクラスに属するかインスタンスに属するかを以下のコードにて確認

if "method" in MyCls.__dict__:
    print u"methodはMyClsの属性です。"
if "method" in my_inst.__dict__:
    print u"methodはmy_instの属性です。"

実行結果

methodはmy_instの属性です。

別のインスタンスを生成してメソッドを実行すると

my_inst2=MyCls()
my_inst2.method()

実行結果

Traceback (most recent call last):
  File "ソースファイルのパス", line XX, in <module>
    my_inst2.method()
AttributeError: 'MyCls' object has no attribute 'method'

メソッドはMyClsの属性では無いので別のインスタンスでは実行できない。


では解説を
特異メソッドを定義するにはtypesモジュールのMethodType型を使う。

typesモジュールにはオブジェクトの型が定義されていて、MethodTypeは「ユーザー定義のクラスのインスタンスのメソッドの型です。」との事。

MethodTypeは型名であるのでpythonではコンストラクタとして使える。
これを使って、ユーザー定義のクラスのインスタンスメソッドオブジェクトを生成できるという事のようだ。

上記のコードではインスタンスに生成したメソッドをバインドしているので、生成したインスタンスメソッドはクラスではなくインスタンスに属する事になり、特定のインスタンスのみで実行できるメソッドになる。
もしこれを、クラスにバインドすれば、当然、通常のインスタンスメソッドになる。
(クラスにバインドしたコードの例については「インスタンスメソッドをバインド」でのサンプルコードを参照)

MethodTypeのコンストラクタについてのドキュメントがみつからず詳しい事はわからなかったが、MethodTypeには引数を3つ(関数とインスタンスとクラスの) 持つコンストラクタもあるようで、特異メソッドのサンプルコードの下から2行目を以下のように置き換えることもできるようだ。

my_inst.method = types.MethodType(func, my_inst, MyCls)

ちなみに、MethodTypeのコンストラクタの引数は、それぞれ特殊属性im_func,im_self,im_classとして使われるようで、確認してみると、

print my_inst.method.im_self
print my_inst.method.im_func
print my_inst.method.im_class

引数を3つ指定した場合には以下のようになる。

<__main__.MyCls object at 0x025BBCD0>
<function func at 0x025C2470>
<class '__main__.MyCls'>

引数を2つの場合には最後のクラス名を指定できないので、im_class属性はNoneに設定される。

余談ではあるが、かっては、newモジュールのinstancemethodを使って同じような事ができたようであるが、newモジュールは「バージョン 2.6 で撤廃」されている。

ページのトップへ戻る