この投稿では,Pythonの基本的な処理である属性名の解決方法について説明します。単純なケースを説明した後,Pythonのインタプリタ内部で実際に行われている詳細なケースを紹介します。使用したPythonのバージョンは3.6.3です。
調べようと思ったきっかけ
unittestパッケージ内のソースコードリーディングを行っている中で以下のコードに遭遇しました。
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行目に到達することになってしまうと考えました。
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の属性の解決を行います。
- 生成されたインスタンスに属性"XXX"があるかチェックする
- インスタンスのクラスに属性"XXX"があるかチェックする
このため,上記のケースのように,インスタンスに属性が代入されていなくても,126行目のresultclass = TextTestResultでクラスに属性が定義されているため,問題なくオブジェクトが返されます。このように属性名の名前解決は,インスタンスをチェックした後,クラスをチェックするのが大原則です。
Pythonの詳細な属性解決の流れ
上記で紹介した属性名の名前解決は正確ではありません。正確な名前解決は下記のように多少複雑な流れになります。
- そのオブジェクトのクラスに属性が存在するか継承元も含めてチェックする
- 1で見つかったその属性の参照先が__get__および__set__を有していれば(データディスクリプタ)__get__をコールしてその結果を返す
- 2に該当しない場合,インスタンス自身の属性辞書を参照。あればそれをそのまま返す
- 1で見つかったその属性の参照先が__get__を有していれば(非データディスクリプタ)__get__をコールしてその結果を返す
- 4に該当しない場合,そのインスタンスのクラスの属性辞書を参照。あればそれをそのまま返す
- 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がメソッドの場合以下の流れでメソッドがコールされます。
- "a.x()"では,"a.x"がまず先に実行され,次に"a.x"の戻り値を"()"に適用します。
- "a.x"の実行は,属性名の名前解決に従います。メソッドは非データディスクリプタに該当するため,__get__の実行結果を返します。
- メソッドの場合,__get__は,types.MethodTypeクラスのオブジェクトを戻り値として返します。
- 戻り値として返された,types.MethodTypeクラスのオブジェクトに対して"()"が適用されます。
- その結果,types.MethodTypeのオブジェクトがコールされ,メソッド本体の実行結果が返されます。
おわりに
上記で紹介したメソッドの仕組み以外にも,属性名の解決の流れを利用して,super()やproperty()といった仕組みを実現しています。このようにPythonの属性名の解決処理は一見複雑ですが,多くの仕組みに応用されている点を考慮すると,一貫性のある非常にシンプルな仕組みだといえます。