18.1.6.2 情報発見

あるアプリケーションでは解析木へ直接アクセスすることが役に立ちます。この節の残りでは、importを使って調査中のコードを実行中のインタープリタにロードする必要も無しに、解析木を使ってdocstringsに定義されたモジュールのドキュメンテーションへのアクセスを可能にする方法を示します。これは信頼性のないコードを解析するためにとても役に立ちます。

一般に、例は興味のある情報を引き出すために解析木をどのような方法でたどればよいかを示しています。二つの関数と一連のクラスが開発され、モジュールが提供する高レベルの関数とクラスの定義をプログラムから利用できるようになります。クラスは情報を解析木から引き出し、便利な意味レベルでその情報へアクセスできるようにします。一つの関数は単純な低レベルのパターンマッチング機能を提供し、もう一つの関数は呼び出し側の代わりにファイル操作を行うという点でクラスへの高レベルなインターフェイスです。ここで言及されていてPythonのインストールに必要ないすべてのソースファイルは、ディストリビューションのDemo/parser/ディレクトリにあります。

Pythonの動的な性質によってプログラマは非常に大きな柔軟性を得ることができます。しかし、クラス、関数およびメソッドを定義するときには、ほとんどのモジュールがこれの限られた部分しか必要としません。この例では、考察される定義だけがコンテキストのトップレベルにおいて定義されるものです。例を挙げると、モジュールのゼロ列目にdef文によって定義される関数で、if ... elseコンストラクトの枝の中に定義されていない関数(ある状況ではそうすることにもっともな理由があるのですが)。例で開発するコードによって、定義の入れ子を扱う予定です。

より上位レベルの抽出メソッドを作るために知る必要があるのは、解析木構造がどのようなものかということと、それのどの程度まで関心を持つ必要があるのかということです。Pythonはやや深い解析木を使いますので、たくさんの中間ノードがあります。Pythonが使う形式文法を読んで理解することは重要です。これは配布物に含まれるファイルGrammar/Grammarに明記されています。docstringsを探すときに対象として最も単純な場合について考えてみてください: docstringの他に何も無いモジュール。(ファイルdocstring.pyを参照してください。)

"""Some documentation.
"""

インタープリタを使って解析木を調べると、数と括弧が途方に暮れるほど多くて、ドキュメンテーションが入れ子になったタプルの深いところに埋まっていることがわかります。

>>> import parser
>>> import pprint
>>> ast = parser.suite(open('docstring.py').read())
>>> tup = ast.totuple()
>>> pprint.pprint(tup)
(257,
 (264,
  (265,
   (266,
    (267,
     (307,
      (287,
       (288,
        (289,
         (290,
          (292,
           (293,
            (294,
             (295,
              (296,
               (297,
                (298,
                 (299,
                  (300, (3, '"""Some documentation.\n"""'))))))))))))))))),
   (4, ''))),
 (4, ''),
 (0, ''))

木の各ノードの最初の要素にある数はノード型です。それらは文法の終端記号と非終端記号に直接に対応します。残念なことに、それらは内部表現の整数で表されていて、生成されたPythonの構造でもそのままになっています。しかし、symboltokenモジュールはノード型の記号名と整数からノード型の記号名へマッピングする辞書を提供します。

上に示した出力の中で、最も外側のタプルは四つの要素を含んでいます: 整数257と三つの付加的なタプル。ノード型257の記号名はfile_inputです。これらの各内部タプルは最初の要素として整数を含んでいます。これらの整数26440は、ノード型stmtNEWLINEENDMARKERをそれぞれ表しています。これらの値はあなたが使っているPythonのバージョンに応じて変化する可能性があることに注意してください。マッピングの詳細については、symbol.pytoken.pyを調べてください。もっとも外側のノードがファイルの内容ではなく入力ソースに主に関係していることはほとんど明らかで、差し当たり無視しても構いません。stmtノードはさらに興味深いです。特に、すべてのdocstringsは、このノードが作られるのとまったく同じように作られ、違いがあるのは文字列自身だけである部分木にあります。同様の木のdocstringと説明の対象である定義されたエンティティ(クラス、関数あるいはモジュール)の関係は、前述の構造を定義している木の内部におけるdocstring部分木の位置によって与えられます。

実際のdocstringを木の変数要素を意味する何かと置き換えることによって、簡単なパターンマッチング方法で与えられたどんな部分木でもdocstringsに対する一般的なパターンと同等かどうかを調べられるようになります。例では情報の抽出の実例を示しているので、['variable_name']という単純な変数表現を念頭において、リスト形式ではなくタプル形式の木を安全に要求できます。簡単な再帰関数でパターンマッチングを実装でき、その関数は真偽値と変数名から値へのマッピングの辞書を返します。(ファイルexample.pyを参照してください。)

from types import ListType, TupleType

def match(pattern, data, vars=None):
    if vars is None:
        vars = {}
    if type(pattern) is ListType:
        vars[pattern[0]] = data
        return 1, vars
    if type(pattern) is not TupleType:
        return (pattern == data), vars
    if len(data) != len(pattern):
        return 0, vars
    for pattern, data in map(None, pattern, data):
        same, vars = match(pattern, data, vars)
        if not same:
            break
    return same, vars

この構文の変数用の簡単な表現と記号のノード型を使うと、docstring部分木の候補のパターンがとても読みやすくなります。(ファイルexample.pyを参照してください。)

import symbol
import token

DOCSTRING_STMT_PATTERN = (
    symbol.stmt,
    (symbol.simple_stmt,
     (symbol.small_stmt,
      (symbol.expr_stmt,
       (symbol.testlist,
        (symbol.test,
         (symbol.and_test,
          (symbol.not_test,
           (symbol.comparison,
            (symbol.expr,
             (symbol.xor_expr,
              (symbol.and_expr,
               (symbol.shift_expr,
                (symbol.arith_expr,
                 (symbol.term,
                  (symbol.factor,
                   (symbol.power,
                    (symbol.atom,
                     (token.STRING, ['docstring'])
                     )))))))))))))))),
     (token.NEWLINE, '')
     ))

このパターンとmatch()関数を使うと、前に作った解析木からモジュールのdocstringを簡単に抽出できます:

>>> found, vars = match(DOCSTRING_STMT_PATTERN, tup[1])
>>> found
1
>>> vars
{'docstring': '"""Some documentation.\n"""'}

特定のデータを期待された位置から抽出できると、次は情報を期待できる場所はどこかという疑問に答える必要がでてきます。docstringを扱う場合、答えはとても簡単です: docstringはコードブロック(file_inputまたはsuiteノード型)の最初のstmtノードです。モジュールは一つのfile_inputノードと、正確にはそれぞれが一つのsuiteノードを含むクラスと関数の定義で構成されます。クラスと関数は(stmt, (compound_stmt, (classdef, ...または(stmt, (compound_stmt, (funcdef, ...で始まるコードブロックノードの部分木として簡単に識別されます。これらの部分木はmatch()によってマッチさせることができないことに注意してください。なぜなら、数を無視して複数の兄弟ノードにマッチすることをサポートしていないからです。この限界を超えるためにより念入りにつくったマッチング関数を使うことができますが、例としてはこれで充分です。

文がdocstringかどうかを決定し、実際の文字列をその文から抽出する機能について考えると、ある作業にはモジュール全体の解析木を巡回してモジュールの各コンテキストにおいて定義される名前についての情報を抽出し、その名前とdocstringsを結び付ける必要があります。この作業を行うコードは複雑ではありませんが、説明が必要です。

そのクラスへの公開インターフェイスは簡単で、おそらく幾分かより柔軟でしょう。モジュールのそれぞれの``主要な''ブロックは、問い合わせのための幾つかのメソッドを提供するオブジェクトと、少なくともそれが表す完全な解析木の部分木を受け取るコンストラクタによって記述されます。ModuleInfoコンストラクタはオプションのnameパラメータを受け取ります。なぜなら、そうしないとモジュールの名前を決められないからです。

公開クラスにはClassInfoFunctionInfoおよびModuleInfoが含まれます。すべてのオブジェクトはメソッドget_name()get_docstring()get_class_names()およびget_class_info()を提供します。ClassInfoオブジェクトはget_method_names()get_method_info()をサポートしますが、他のクラスはget_function_names()get_function_info()を提供しています。

公開クラスが表すコードブロックの形式のそれぞれにおいて、トップレベルで定義された関数が``メソッド''として参照されるという違いがクラスにはありますが、要求される情報のほとんどは同じ形式をしていて、同じ方法でアクセスされます。クラスの外側で定義される関数との実際の意味の違いを名前の付け方が違うことで反映しているため、実装はこの違いを保つ必要があります。そのため、公開クラスのほとんどの機能が共通の基底クラスSuiteInfoBaseに実装されており、他の場所で提供される関数とメソッドの情報に対するアクセサを持っています。関数とメソッドの情報を表すクラスが一つだけであることに注意してください。これは要素の両方の型を定義するためにdef文を使うことに似ています。

アクセサ関数のほとんどはSuiteInfoBaseで宣言されていて、サブクラスでオーバーライドする必要はありません。より重要なこととしては、解析木からのほとんどの情報抽出がSuiteInfoBaseコンストラクタに呼び出されるメソッドを通して行われるということがあります。平行して形式文法を読めば、ほとんどのクラスのコード例は明らかです。しかし、再帰的に新しい情報オブジェクトを作るメソッドはもっと調査が必要です。example.pySuiteInfoBase定義の関連する箇所を以下に示します:

class SuiteInfoBase:
    _docstring = ''
    _name = ''

    def __init__(self, tree = None):
        self._class_info = {}
        self._function_info = {}
        if tree:
            self._extract_info(tree)

    def _extract_info(self, tree):
        # extract docstring
        if len(tree) == 2:
            found, vars = match(DOCSTRING_STMT_PATTERN[1], tree[1])
        else:
            found, vars = match(DOCSTRING_STMT_PATTERN, tree[3])
        if found:
            self._docstring = eval(vars['docstring'])
        # discover inner definitions
        for node in tree[1:]:
            found, vars = match(COMPOUND_STMT_PATTERN, node)
            if found:
                cstmt = vars['compound']
                if cstmt[0] == symbol.funcdef:
                    name = cstmt[2][1]
                    self._function_info[name] = FunctionInfo(cstmt)
                elif cstmt[0] == symbol.classdef:
                    name = cstmt[2][1]
                    self._class_info[name] = ClassInfo(cstmt)

初期状態に初期化した後、コンストラクタは_extract_info()メソッドを呼び出します。このメソッドがこの例全体で行われる情報抽出の大部分を実行します。抽出には二つの別々の段階があります: 渡された解析木のdocstringの位置の特定、解析木が表すコードブロック内の付加的な定義の発見。

最初のifテストは入れ子のsuiteが``短い形式''または``長い形式''かどうかを決定します。以下のコードブロックの定義のように、コードブロックが同じ行であるときに短い形式が使われます。

def square(x): "Square an argument."; return x ** 2

長い形式では字下げされたブロックを使い、入れ子になった定義を許しています:

def make_power(exp):
    "Make a function that raises an argument to the exponent `exp'."
    def raiser(x, y=exp):
        return x ** y
    return raiser

短い形式が使われるとき、コードブロックはdocstringを最初のsmall_stmt要素として(ことによるとそれだけを)持っています。このようなdocstringの抽出は少し異なり、より一般的な場合に使われる完全なパターンの一部だけを必要とします。実装されているように、simple_stmtノードにsmall_stmtノードが一つだけある場合には、docstringしかないことがあります。短い形式を使うほとんどの関数とメソッドがdocstringを提供しないため、これで充分だと考えられます。docstringの抽出は前述のmatch()関数を使って進み、docstringがSuiteInfoBaseオブジェクトの属性として保存されます。

docstringを抽出した後、簡単な定義発見アルゴリズムをsuiteノードのstmtノードに対して実行します。短い形式の特別な場合はテストされません。短い形式ではstmtノードが存在しないため、アルゴリズムは黙ってsimple_stmtノードを一つスキップします。正確に言えば、どんな入れ子になった定義も発見しません。

コードブロックのそれぞれの文をクラス定義(関数またはメソッドの定義、あるいは、何か他のもの)として分類します。定義文に対しては、定義された要素の名前が抽出され、コンストラクタに引数として渡される部分木の定義とともに定義に適した代理オブジェクトが作成されます。代理オブジェクトはインスタンス変数に保存され、適切なアクセサメソッドを使って名前から取り出されます。

公開クラスはSuiteInfoBaseクラスが提供するアクセサより具体的で、必要とされるどんなアクセサでも提供します。しかし、実際の抽出アルゴリズムはコードブロックのすべての形式に対して共通のままです。高レベルの関数をソースファイルから完全な情報のセットを抽出するために使うことができます。(ファイルexample.pyを参照してください。)

def get_docs(fileName):
    import os
    import parser

    source = open(fileName).read()
    basename = os.path.basename(os.path.splitext(fileName)[0])
    ast = parser.suite(source)
    return ModuleInfo(ast.totuple(), basename)

これはモジュールのドキュメンテーションに対する使いやすいインターフェイスです。この例のコードで抽出されない情報が必要な場合は、機能を追加するための明確に定義されたところで、コードを拡張することができます。

ご意見やご指摘をお寄せになりたい方は、 このドキュメントについて... をご覧ください。