LoginSignup
1

More than 5 years have passed since last update.

Pythonにおける属性の解決手順

Posted at

この投稿では,Pythonの基本的な処理である属性名の解決方法について説明します。単純なケースを説明した後,Pythonのインタプリタ内部で実際に行われている詳細なケースを紹介します。使用したPythonのバージョンは3.6.3です。

調べようと思ったきっかけ

unittestパッケージ内のソースコードリーディングを行っている中で以下のコードに遭遇しました。

unittest/runner.py
148    def _makeResult(self):
149        return self.resultclass(self.stream, self.descriptions, self.verbosity)
150

上記でself.resultclassをコールしているため,self.resultclassはメソッドもしくはクラスで予めどこかで代入処理がされていると思いましたが,代入されること無く上記の149行目に到達するケースがあることがわかりました。以下は関係するコードの抜粋ですが,self.resultclassに対する代入処理はソースコード内で146行目のみでした。その上の条件式がFalseの場合,代入処理が行われる未定義のまま149行目に到達することになってしまうと考えました。

unittest/runner.py
120 class TextTestRunner(object):

126        resultclass = TextTestResult
127 
128        def __init__(self, ...
129                      ..., resultclass=None, ...):
                ...                 
145             if resultclass is not None:
146                 self.resultclass = resultclass
147    
148         def _makeResult(self):
149             return self.resultclass(self.stream, self.descriptions, self.verbosity)
150    
151         def run(self, test):
152            ...
153            result = self._makeResult()
154               ...

Pythonにおける属性名の解決(簡略版)

上記の考えは誤りです。Pythonのselfでは,以下の順序でself.XXXの属性の解決を行います。

  1. 生成されたインスタンスに属性"XXX"があるかチェックする
  2. インスタンスのクラスに属性"XXX"があるかチェックする

このため,上記のケースのように,インスタンスに属性が代入されていなくても,126行目のresultclass = TextTestResultでクラスに属性が定義されているため,問題なくオブジェクトが返されます。このように属性名の名前解決は,インスタンスをチェックした後,クラスをチェックするのが大原則です。

Pythonの詳細な属性解決の流れ

上記で紹介した属性名の名前解決は正確ではありません。正確な名前解決は下記のように多少複雑な流れになります。

  1. そのオブジェクトのクラスに属性が存在するか継承元も含めてチェックする
  2. 1で見つかったその属性の参照先が__get__および__set__を有していれば(データディスクリプタ)__get__をコールしてその結果を返す
  3. 2に該当しない場合,インスタンス自身の属性辞書を参照。あればそれをそのまま返す
  4. 1で見つかったその属性の参照先が__get__を有していれば(非データディスクリプタ)__get__をコールしてその結果を返す
  5. 4に該当しない場合,そのインスタンスのクラスの属性辞書を参照。あればそれをそのまま返す
  6. 2〜5すべてに該当しない場合はAttributeErrorを返す

簡略版との大きな違いは,データディスクリプタと非データディスクリプタのケースがあることです。データディスクリプタは,Pythonでプロパティを実現するために必要となります。また,非データディスクリプタを利用した代表例はメソッドになります。

余談ですが,上記の処理は__getattribute__特殊メソッドのデフォルトの動作です。このため,__getattribute__をオーバライドすることによって上記の名前解決の流れをカスタマイズすることができます。

関数(メソッド)の呼び出し

Pythonは上記の仕組みを利用してメソッドの呼び出しを工夫することで実現しています。メソッドの実装はC言語で記述されているため(CPythonの場合),擬似コードで示します。

class Function(object):
    def __get__(self, obj, klass=None):
        if obj is None:
            return self
        return types.MethodType(self, obj)

メソッドは上記のように__get__特殊メソッドを保持したクラスのインスタンスとして内部で扱われるため,非データディスクリプタとなります。属性名の名前解決における非データディスクリプタの特徴は,ただ参照先を返すのではなく,__get__の実行結果を返す点にあります。それに従うと,メソッドはtypes.MethodType(self, obj)の戻り値を返します。このtypes.MethodType(self, obj)が返すオブジェクトは,コールされると下記の疑似コードを実行します。

def method_call(meth, *args, **kw):
    self = meth.__self__
    func = meth.__func__
    return func(self, *args, **kw)

このようにMethodTypeのオブジェクトは,生成される過程で保持した__self__と__func__をコールされるタイミングで取り出し,組み立てなおして__func__をコールします。余談ですが,defによってメソッドを定義する際,第1引数にselfを指定する理由はこれです。

属性名の解決と関数(メソッド)呼び出しの整理

上記のことをふまえて関数の呼び出しを属性名の解決の観点からもう一度整理します。例えば"a.x()"というコードがありxがメソッドの場合以下の流れでメソッドがコールされます。

  1. "a.x()"では,"a.x"がまず先に実行され,次に"a.x"の戻り値を"()"に適用します。
  2. "a.x"の実行は,属性名の名前解決に従います。メソッドは非データディスクリプタに該当するため,__get__の実行結果を返します。
  3. メソッドの場合,__get__は,types.MethodTypeクラスのオブジェクトを戻り値として返します。
  4. 戻り値として返された,types.MethodTypeクラスのオブジェクトに対して"()"が適用されます。
  5. その結果,types.MethodTypeのオブジェクトがコールされ,メソッド本体の実行結果が返されます。

おわりに

上記で紹介したメソッドの仕組み以外にも,属性名の解決の流れを利用して,super()やproperty()といった仕組みを実現しています。このようにPythonの属性名の解決処理は一見複雑ですが,多くの仕組みに応用されている点を考慮すると,一貫性のある非常にシンプルな仕組みだといえます。
 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1