はじめに
Pythonのプロジェクトが大きくなると、モジュール間の依存関係を把握することが難しくなります。この記事では、PythonのAST(Abstract Syntax Tree)モジュールを用いてコードベースを解析し、NetworkXとPlotlyを使ってモジュール間のインポート関係を可視化する方法を紹介します。
目次
ディレクトリ構成
以下のようなディレクトリ構成のプロジェクトを例として使用します。
※機械学習プロジェクトを意識した例としましたが、任意のディレクトリ構成に対応しています。
visualize_imports_relationship.py
が可視化ソースコードです。それ以外は、サンプルコードとなっています。
src/
├── visualize_imports_relationship.py
├── main.py
├── data_processing/
│ ├── data_loader.py
│ ├── data_cleaner.py
│ └── __init__.py
├── models/
│ ├── base_model.py
│ ├── linear_model.py
│ ├── neural_network.py
│ └── __init__.py
├── utils/
│ ├── logger.py
│ ├── config.py
│ └── __init__.py
ソースコードの説明
依存関係の解析と可視化を行うソースコード
以下のコードは、プロジェクト内のPythonファイルを解析し、モジュール間のインポート関係をNetworkXでグラフ化し、Plotlyで可視化するものです。
import ast
import fnmatch
import os
import networkx as nx
import plotly.graph_objects as go
EXCLUDE_PATTERN = ["test_*.py", "visualize_imports_relationship.py", "*__init__.py"]
def find_imports(file_path):
"""
指定されたファイルのインポートを解析します。
Args:
file_path (str): ファイルパス。
Returns:
list[str]: インポートされたモジュールのリスト。
"""
with open(file_path, "r", encoding="utf-8") as f:
tree = ast.parse(f.read(), filename=file_path)
imports = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
imports.append(alias.name)
elif isinstance(node, ast.ImportFrom):
module = node.module if node.module else ""
for n in node.names:
if module:
imports.append(f"{module}.{n.name}")
else:
imports.append(n.name)
return imports
def analyze_codebase(src_root):
"""
ディレクトリ内のPythonファイルを解析し、インポートを収集します。
Args:
src_root (str): ソースコードのルートディレクトリ。
Returns:
tuple: ファイルごとのインポートリストとモジュール名からファイルパスへのマッピング。
"""
file_imports = {}
module_name_to_path = {}
for root, _, files in os.walk(src_root):
for file in files:
if not file.endswith(".py"):
continue
file_path = os.path.join(root, file)
rel_path = os.path.relpath(file_path, src_root)
if any(fnmatch.fnmatch(rel_path, pattern) for pattern in EXCLUDE_PATTERN):
continue
module_name = os.path.splitext(rel_path)[0].replace(os.sep, ".")
module_name = module_name.lstrip(".") # 不要な先頭のドットを削除
full_module_name = f"{src_root}.{module_name}"
module_name_to_path[full_module_name] = rel_path
imports = find_imports(file_path)
file_imports[rel_path] = imports
return file_imports, module_name_to_path
def find_custom_modules(file_imports, module_name_to_path):
"""
自作モジュールのみのインポートを抽出します。
Args:
file_imports (dict): ファイルごとのインポートリスト。
module_name_to_path (dict): モジュール名からファイルパスへのマッピング。
Returns:
dict: 自作モジュールのみのインポートリスト。
"""
custom_imports = {}
module_names = set(module_name_to_path.keys())
for filepath, imports in file_imports.items():
custom_only = [imp for imp in imports if imp in module_names]
custom_imports[filepath] = custom_only
return custom_imports
def build_dependency_graph(custom_imports, module_name_to_path):
"""
ファイル間の依存関係グラフを構築します。
Args:
custom_imports (dict): 自作モジュールのみのインポートリスト。
module_name_to_path (dict): モジュール名からファイルパスへのマッピング。
Returns:
nx.DiGraph: 依存関係グラフ。
"""
G = nx.DiGraph()
for filepath, imports in custom_imports.items():
for imp in imports:
if imp in module_name_to_path:
imp_relpath = module_name_to_path[imp]
G.add_edge(filepath, imp_relpath)
return G
def visualize_graph(graph):
"""
依存関係グラフを可視化します。
Args:
graph (nx.DiGraph): 依存関係グラフ。
"""
pos = nx.spring_layout(graph)
edge_trace = []
node_trace = go.Scatter(
x=[],
y=[],
text=[],
mode="markers+text",
textposition="top center",
marker=dict(
showscale=True, colorscale="YlGnBu", color=[], size=[], sizemode="area"
),
)
for node in graph.nodes():
x, y = pos[node]
node_trace["x"] += (x,)
node_trace["y"] += (y,)
node_trace["text"] += (node,)
node_degree = graph.degree(node)
node_trace["marker"]["size"] += (1000 + node_degree * 200,)
node_trace["marker"]["color"] += (node_degree,)
for edge in graph.edges():
x0, y0 = pos[edge[0]]
x1, y1 = pos[edge[1]]
edge_trace.append(
go.Scatter(
x=(x0, x1, None),
y=(y0, y1, None),
line=dict(width=1, color="black"),
mode="lines",
)
)
fig = go.Figure(
data=edge_trace + [node_trace],
layout=go.Layout(
showlegend=False,
hovermode="closest",
margin=dict(b=20, l=5, r=5, t=40),
xaxis=dict(showgrid=False, zeroline=False),
yaxis=dict(showgrid=False, zeroline=False),
),
)
fig.show()
if __name__ == "__main__":
src_root = "src"
file_imports, module_name_to_path = analyze_codebase(src_root)
print("File Imports:", file_imports)
print("Module Name to Path:", module_name_to_path)
custom_imports = find_custom_modules(file_imports, module_name_to_path)
print("Custom Imports:", custom_imports)
dependency_graph = build_dependency_graph(custom_imports, module_name_to_path)
print("Edges in Dependency Graph:", list(dependency_graph.edges()))
visualize_graph(dependency_graph)
各モジュールの内容
以下はサンプルコードです。
※内容は何でもOKです。
src/main.py
from src.data_processing import data_loader
from src.models import linear_model
from src.utils import config, logger
def main():
logger_instance = logger.Logger()
data = data_loader.load_data()
model = linear_model.LinearModel()
model.train(data)
logger_instance.log(config.CONFIG)
if __name__ == "__main__":
main()
src/data_processing/data_loader.py
from src.data_processing import data_cleaner
def load_data():
raw_data = "raw data"
data = data_cleaner.clean_data(raw_data)
return data
src/data_processing/data_cleaner.py
def clean_data(data):
return f"cleaned {data}"
src/models/base_model.py
class BaseModel:
def train(self, data):
raise NotImplementedError("Train method must be implemented by subclasses.")
src/models/linear_model.py
from src.models import base_model
class LinearModel(base_model.BaseModel):
def train(self, data):
print(f"Training Linear Model with {data}")
src/models/neural_network.py
from src.models import base_model
class NeuralNetwork(base_model.BaseModel):
def train(self, data):
print(f"Training Neural Network with {data}")
src/utils/logger.py
class Logger:
def log(self, message):
print(f"[LOG]: {message}")
src/utils/config.py
CONFIG = {"learning_rate": 0.01, "epochs": 10}
実行結果
上記のコードを実行すると、以下のようなモジュール間の依存関係グラフが表示されます。
- ノードは各Pythonファイルを表し、エッジはインポート関係を示しています。
- ノードのサイズと色は、そのノードの次数(他のノードとの接続数)に基づいています。
コンソール出力例:
python src/visualize_imports_relationship.py
File Imports: {'main.py': ['src.data_processing.data_loader', 'src.models.linear_model', 'src.utils.config', 'src.utils.logging'], 'data\\data.py': [], 'data_processing\\data_cleaner.py': [], 'data_processing\\data_loader.py': ['src.data_processing.data_cleaner'], 'models\\base_model.py': [], 'models\\linear_model.py': ['src.models.base_model'], 'models\\neural_network.py': ['src.models.base_model'], 'utils\\config.py': [], 'utils\\logging.py': []}
Module Name to Path: {'src.main': 'main.py', 'src.data.data': 'data\\data.py', 'src.data_processing.data_cleaner': 'data_processing\\data_cleaner.py', 'src.data_processing.data_loader': 'data_processing\\data_loader.py', 'src.models.base_model': 'models\\base_model.py', 'src.models.linear_model': 'models\\linear_model.py', 'src.models.neural_network': 'models\\neural_network.py', 'src.utils.config': 'utils\\config.py', 'src.utils.logging': 'utils\\logging.py'}
Custom Imports: {'main.py': ['src.data_processing.data_loader', 'src.models.linear_model', 'src.utils.config', 'src.utils.logging'], 'data\\data.py': [], 'data_processing\\data_cleaner.py': [], 'data_processing\\data_loader.py': ['src.data_processing.data_cleaner'], 'models\\base_model.py': [], 'models\\linear_model.py': ['src.models.base_model'], 'models\\neural_network.py': ['src.models.base_model'], 'utils\\config.py': [], 'utils\\logging.py': []}
Edges in Dependency Graph: [('main.py', 'data_processing\\data_loader.py'), ('main.py', 'models\\linear_model.py'), ('main.py', 'utils\\config.py'), ('main.py', 'utils\\logging.py'), ('data_processing\\data_loader.py', 'data_processing\\data_cleaner.py'), ('models\\linear_model.py', 'models\\base_model.py'), ('models\\neural_network.py', 'models\\base_model.py')]
まとめ
この方法を使うことで、プロジェクト内のモジュール間の依存関係を視覚的に把握することができます。特に大規模なプロジェクトでは、依存関係の循環や複雑な関係性を発見するのに役立ちます。
ポイント:
- PythonのASTモジュールを使用してコードを解析。
- NetworkXで依存関係グラフを構築。
- Plotlyでグラフを可視化。
- 自作モジュールのみを対象にすることで、外部ライブラリの影響を除外。