ディレクトリ構造を超えて：関数・クラス・メソッドまで含むPythonツリービューアの作成

## はじめに

Pythonプロジェクトが大規模になるにつれて、そのコードベース全体を理解することがますます難しくなります。特に、ディレクトリ内のファイルが増えると、どのファイルにどの関数やクラスが含まれているのかを把握するのは至難の業です。このような状況で役立つのが、ディレクトリ構造に加えて、各ファイルに含まれる関数やクラス・メソッドまでを視覚的に表示するツリービューアです。

本記事では、Pythonスクリプトを使用してディレクトリ構造だけでなく、その配下にある関数、クラス、メソッドを階層的に表示する方法について解説します。このツリービューアを使えば、コードベースの全体像を簡単に把握でき、開発作業の効率化が図れます。

## 必要性と用途

ディレクトリ構造だけを表示するだけでは、複雑なコードベースの理解には不十分です。特に、大規模プロジェクトでは、どのファイルにどのような関数やクラスが定義されているのかを一目で確認できることが重要です。このようなニーズに応えるために、関数、クラス、メソッドまでを含むツリービューアが必要になります。

このツリービューアは、プロジェクトのコードレビュー、リファクタリング、新規開発時のコード理解に役立ちます。また、新しくプロジェクトに参加したメンバーがコードベースを迅速に理解するのにも有用です。

## スクリプトの設計

今回作成するスクリプトは、以下のような構成を持っています。

* `DirectoryStructure`クラス: 指定されたディレクトリ内のPythonファイルを再帰的に検索し、それらのファイルから関数、クラス、メソッドを抽出します。また、無視したいフォルダを指定することができます。
* `DirectoryPrinter`クラス: `DirectoryStructure`クラスを用いて解析されたディレクトリ構造を、ツリービュー形式で表示します。

このスクリプトは、無視するフォルダを簡単に設定できるように設計されています。例えば、`__pycache__`のようなキャッシュフォルダを無視することで、必要な情報だけを効率的に表示することが可能です。

## コード解説

このセクションでは、スクリプトの各部分を詳細に解説します。

### ディレクトリとPythonファイルのリストアップ

`DirectoryStructure`クラスでは、`list_py_files`メソッドを使用して、指定されたディレクトリ内のすべてのPythonファイルを再帰的にリストアップします。このメソッドは、無視したいフォルダを指定する機能も持ち、不要なファイルをリストアップしないようにします。

```python
def list_py_files(self) -> List[Path]:
    py_files = []
    for path in self.directory.rglob('*.py'):
        if not any(ignored in path.parts for ignored in self.ignore_folders):
            py_files.append(path)
    return py_files
```

### ASTを用いた関数・クラス・メソッドの抽出

Pythonの標準ライブラリに含まれるAST（Abstract Syntax Tree）モジュールを使用して、各Pythonファイル内の関数、クラス、およびメソッドを解析し、その名前を抽出します。`extract_definitions`メソッドがこの処理を担当します。

```python
def extract_definitions(self, node: ast.AST, indent_level: int = 1) -> List[str]:
    indent = ' ' * 4 * indent_level
    definitions = []

    if isinstance(node, ast.FunctionDef):
        definitions.append(f"{indent}Function: {node.name}()")
    
    elif isinstance(node, ast.ClassDef):
        definitions.append(f"{indent}Class: {node.name}")
        for child in node.body:
            if isinstance(child, ast.FunctionDef):
                definitions.append(f"{indent}    Method: {child.name}()")
    
    return definitions
```

### ディレクトリ構造の出力

`DirectoryPrinter`クラスは、`DirectoryStructure`クラスを使用して解析されたディレクトリ構造をツリービュー形式で出力します。この出力では、ディレクトリとファイルの階層に応じてインデントが調整され、各ファイルに含まれる関数、クラス、メソッドが階層的に表示されます。

```python
def print_structure(self) -> None:
    for path in sorted(self.structure.directory.rglob('*')):
        if any(ignored in path.parts for ignored in self.structure.ignore_folders):
            continue

        level = len(path.relative_to(self.structure.directory).parts) - 1
        indent = ' ' * 4 * level

        if path.is_dir():
            print(f'{indent}{path.name}/')
        elif path.suffix == '.py':
            print(f'{indent}{path.name}')
            definitions = self.structure.parse_file_structure(path)
            for definition in definitions:
                print(f'{indent}    {definition}')
```

### 全コード

```python
import ast
from pathlib import Path
from typing import List, Set


class DirectoryStructure:
    """
    ディレクトリの構造を解析するクラス。
    指定されたディレクトリ内のPythonファイルをリストアップし、
    それぞれのファイルから関数、クラス、メソッドを抽出します。
    """

    def __init__(self, directory: str, ignore_folders: Set[str] = None):
        """
        クラスの初期化メソッド。

        Args:
            directory (str): 解析するディレクトリのパス。
            ignore_folders (Set[str], optional): 無視するフォルダのセット。デフォルトはNone。
        """
        self.directory = Path(directory)
        self.ignore_folders = ignore_folders if ignore_folders else set()

    def list_py_files(self) -> List[Path]:
        """
        指定されたディレクトリ内のすべてのPythonファイルを再帰的にリストアップします。
        無視するフォルダ内のファイルはリストアップしません。

        Returns:
            List[Path]: Pythonファイルのパスのリスト。
        """
        py_files = []
        for path in self.directory.rglob("*.py"):
            if not any(ignored in path.parts for ignored in self.ignore_folders):
                py_files.append(path)
        return py_files

    def extract_definitions(self, node: ast.AST, indent_level: int = 1) -> List[str]:
        """
        ASTノードから関数、クラス、およびメソッドの定義を抽出します。

        Args:
            node (ast.AST): ASTノード。
            indent_level (int): インデントのレベル。

        Returns:
            List[str]: 関数、クラス、およびメソッドのリスト。
        """
        indent = " " * 4 * indent_level
        definitions = []

        if isinstance(node, ast.FunctionDef):
            definitions.append(f"{indent}Function: {node.name}()")

        elif isinstance(node, ast.ClassDef):
            definitions.append(f"{indent}Class: {node.name}")
            for child in node.body:
                if isinstance(child, ast.FunctionDef):
                    definitions.append(f"{indent}    Method: {child.name}()")

        return definitions

    def parse_file_structure(self, py_file: Path) -> List[str]:
        """
        指定されたPythonファイルの構造を解析し、関数、クラス、およびメソッドを抽出します。

        Args:
            py_file (Path): Pythonファイルのパス。

        Returns:
            List[str]: ファイル内の関数、クラス、およびメソッドのリスト。
        """
        with open(py_file, "r", encoding="utf-8") as file:
            tree = ast.parse(file.read(), filename=py_file)

        definitions = []
        for node in tree.body:
            if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
                definitions.extend(self.extract_definitions(node))

        return definitions


class DirectoryPrinter:
    """
    ディレクトリの構造を表示するクラス。
    DirectoryStructureクラスを使用してディレクトリを解析し、結果を表示します。
    """

    def __init__(self, directory: str, ignore_folders: Set[str] = None):
        """
        クラスの初期化メソッド。

        Args:
            directory (str): 表示するディレクトリのパス。
            ignore_folders (Set[str], optional): 無視するフォルダのセット。デフォルトはNone。
        """
        self.structure = DirectoryStructure(directory, ignore_folders)

    def print_structure(self) -> None:
        """
        ディレクトリの構造を表示します。
        各ディレクトリとファイルの階層を反映して、インデントを調整します。
        """
        for path in sorted(self.structure.directory.rglob("*")):
            if any(ignored in path.parts for ignored in self.structure.ignore_folders):
                continue

            level = len(path.relative_to(self.structure.directory).parts) - 1
            indent = " " * 4 * level

            if path.is_dir():
                print(f"{indent}{path.name}/")
            elif path.suffix == ".py":
                print(f"{indent}{path.name}")
                definitions = self.structure.parse_file_structure(path)
                for definition in definitions:
                    print(f"{indent}    {definition}")


def main():
    # 実行するディレクトリを指定します。
    directory = "src"
    ignore_folders = {"__pycache__"}

    printer = DirectoryPrinter(directory=directory, ignore_folders=ignore_folders)
    printer.print_structure()


if __name__ == "__main__":
    main()
```

## カスタマイズ方法

このスクリプトは、ユーザーが特定のニーズに合わせてカスタマイズできるように設計されています。

### 無視するフォルダの指定

`ignore_folders`変数を使って、無視するフォルダを簡単に指定できます。これにより、特定のフォルダ内のファイルがリストアップされないようにすることが可能です。

```python
ignore_folders = {'__pycache__'}
printer = DirectoryPrinter(directory, ignore_folders)
```

### 出力フォーマットの調整

出力されるツリービューのフォーマットは、インデントや表示形式を変更することで調整可能です。必要に応じて、スクリプト内のインデント設定や文字列フォーマットを変更してください。

## 実行例と結果

実際にスクリプトを実行してみましょう。以下は、`フォルダ/配下フォルダ/モジュール`ディレクトリに対してスクリプトを実行した結果の例です。

```bash
フォルダ/
    配下フォルダ/
        モジュール/
            example1.py
                クラス: MyClass
                    メソッド: method1()
                    メソッド: method2()
                関数: func1()
            example2.py
                クラス: AnotherClass
                    メソッド: method1()
            サブフォルダ/
                example3.py
                    関数: func2()
```

このように、ディレクトリ構造とともに、各Pythonファイル内のクラス、メソッド、関数が階層的に表示されます。


## まとめと今後の展望

今回紹介したスクリプトを使うことで、Pythonプロジェクトのディレクトリ構造を詳細に把握でき、開発効率の向上が期待できます。特に、複雑なプロジェクトでのコードレビューやリファクタリング作業を支援するツールとして有用です。

今後の展望としては、以下のような機能拡張が考えられます。

* 出力フォーマットのカスタマイズ: 出力形式をさらに細かく調整可能にし、ユーザーのニーズに合わせた表示ができるようにする。
* 他のプログラミング言語への対応: Python以外のプログラミング言語にも対応できるようにスクリプトを拡張する。
* LLM（大規模言語モデル）の活用: LLMを活用して、各関数やクラス、メソッドの説明を自動生成し、ツリービューに表示する機能を追加する。これにより、コードの理解がさらに深まり、ドキュメンテーション作業の負担が軽減される。

このツリービューアをプロジェクトに導入することで、コードベース全体の理解が深まり、開発プロセスの効率化が図られることを願っています。