user:
次の関数でWidgetに出てくるボタンの数が多くなりすぎると、ボタンがつぶれて見えにくい(スクロールは使えない)問題があります。改善した関数の完全コードを書いていただけますか?
import os
import importlib.util
import inspect
from IPython.display import display, HTML, clear_output
import ipywidgets as widgets
def browse_user_functions(module_path):
"""
指定したモジュールまたはディレクトリからPythonモジュールを再帰的に探し、
モジュール内の関数名リストをJupyter上に表示。
関数名をクリックすると、その関数のdocstringを表示するインタラクティブウィジェット。
Args:
module_path (str): モジュールファイルのパス、またはモジュールディレクトリのパス。
"""
# 指定パスがファイルなら、そのディレクトリをモジュール探索ディレクトリとして扱い、
# ファイルは単体モジュールとして読み込み対象とする
modules_info = [] # (module_name, module_object)
def load_module_from_path(file_path, module_base_name=None):
# module_base_name 省略時はファイル名から自動生成
# module_nameはファイル名(.pyなし)などにする
if module_base_name is None:
module_base_name = os.path.splitext(os.path.basename(file_path))[0]
spec = importlib.util.spec_from_file_location(module_base_name, file_path)
if spec is None or spec.loader is None:
print(f"ロード失敗: {file_path}")
return None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def recursive_find_modules(dir_path, base_module_name=""):
"""
再帰でディレクトリから.pyファイルを見つけてモジュール名を
ベースにしてモジュールとしてロードする。
base_module_nameは空文字列なら単純モジュール名になる。
"""
found = []
for root, dirs, files in os.walk(dir_path):
# __init__.pyがあればそこをモジュールとみなしてサブパッケージ判定可だが、
# ここでは単純にファイル毎にロードするだけにする
# rootからdir_pathの相対パスを取得
rel_path = os.path.relpath(root, dir_path)
if rel_path == '.':
rel_module_prefix = ""
else:
# 相対パスをモジュールパスに変換(ディレクトリ名をドットでつなぐ)
rel_module_prefix = rel_path.replace(os.sep, ".") + "."
for f in files:
if f.endswith(".py") and not f.startswith("__"):
module_name = base_module_name + rel_module_prefix + f[:-3]
file_path = os.path.join(root, f)
found.append( (module_name, file_path) )
return found
# --- モジュールの読み込み処理 ---
modules_info = []
if os.path.isdir(module_path):
# ディレクトリ
modules_list = recursive_find_modules(module_path)
for mod_name, fpath in modules_list:
try:
mod_obj = load_module_from_path(fpath, mod_name)
if mod_obj:
modules_info.append( (mod_name, mod_obj) )
except Exception as e:
print(f"モジュール読み込みエラー: {mod_name} ({fpath}): {e}")
elif os.path.isfile(module_path) and module_path.endswith(".py"):
# 単一ファイル
mod_name = os.path.splitext(os.path.basename(module_path))[0]
mod_obj = load_module_from_path(module_path, mod_name)
if mod_obj:
modules_info.append( (mod_name, mod_obj) )
else:
print("指定されたmodule_pathが.pyファイルかディレクトリではありません。")
return
# --- すべての関数を集めて、(モジュール名, 関数名, 関数オブジェクト) のリストを作る ---
all_functions = []
for mod_name, mod_obj in modules_info:
funcs = inspect.getmembers(mod_obj, inspect.isfunction)
for fname, fobj in funcs:
all_functions.append( (mod_name, fname, fobj) )
# --- ウィジェットの作成 ---
output = widgets.Output()
def on_function_clicked(mod_name, fname, fobj):
with output:
clear_output()
doc = fobj.__doc__
if doc is None or doc.strip() == "":
doc = "(この関数にはドキュメンテーション文字列がありません)"
display(HTML(f"<h3>{mod_name}.{fname}</h3><pre>{doc}</pre>"))
# 関数名リストのボタン(またはリンク)を作成
buttons = []
for mod_name, fname, fobj in all_functions:
btn = widgets.Button(description=f"{mod_name}.{fname}")
btn.style.button_color = "#eee"
btn.layout.width = "auto" # 横幅自動調整
# クロージャ対策でlambda内に引数明示的に渡す
btn.on_click(lambda ev, mn=mod_name, fn=fname, fo=fobj: on_function_clicked(mn, fn, fo))
buttons.append(btn)
# 縦スクロールができるBoxにまとめる
buttons_box = widgets.VBox(buttons,
layout=widgets.Layout(
border="1px solid gray",
height="300px",
overflow_y="scroll",
width="300px",
padding="5px"
))
display(widgets.HBox([buttons_box, output]))
# 使い方例:
# browse_user_functions("../src")
assistant:
import os
import importlib.util
import inspect
from IPython.display import display, HTML, clear_output
import ipywidgets as widgets
def browse_user_functions(module_path, *, height="360px", left_width="420px"):
"""
指定したモジュールまたはディレクトリからPythonモジュールを再帰的に探し、
モジュール内の関数名をJupyter上に表示。
関数名をクリックすると、その関数のdocstringを表示するインタラクティブウィジェット。
改善点:
- ボタン大量問題を解消するため「ボタン群」をやめ、検索可能なDropdown + 検索ボックスに変更
(Jupyterのウィジェットでスクロール不可/潰れる問題を回避)
- モジュール/関数でフィルタ可能
- docstring表示は右側Outputに表示
Args:
module_path (str): モジュールファイル(.py)のパス、またはモジュールディレクトリのパス。
height (str): 左パネルの高さ(例 "360px")
left_width (str): 左パネルの幅(例 "420px")
"""
# --------------------------
# モジュール読み込み関連
# --------------------------
def load_module_from_path(file_path, module_base_name=None):
if module_base_name is None:
module_base_name = os.path.splitext(os.path.basename(file_path))[0]
spec = importlib.util.spec_from_file_location(module_base_name, file_path)
if spec is None or spec.loader is None:
print(f"ロード失敗: {file_path}")
return None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def recursive_find_modules(dir_path, base_module_name=""):
found = []
for root, _, files in os.walk(dir_path):
rel_path = os.path.relpath(root, dir_path)
rel_module_prefix = "" if rel_path == "." else rel_path.replace(os.sep, ".") + "."
for f in files:
if f.endswith(".py") and not f.startswith("__"):
module_name = base_module_name + rel_module_prefix + f[:-3]
file_path = os.path.join(root, f)
found.append((module_name, file_path))
return found
modules_info = []
if os.path.isdir(module_path):
modules_list = recursive_find_modules(module_path)
for mod_name, fpath in modules_list:
try:
mod_obj = load_module_from_path(fpath, mod_name)
if mod_obj:
modules_info.append((mod_name, mod_obj))
except Exception as e:
print(f"モジュール読み込みエラー: {mod_name} ({fpath}): {e}")
elif os.path.isfile(module_path) and module_path.endswith(".py"):
mod_name = os.path.splitext(os.path.basename(module_path))[0]
try:
mod_obj = load_module_from_path(module_path, mod_name)
if mod_obj:
modules_info.append((mod_name, mod_obj))
except Exception as e:
print(f"モジュール読み込みエラー: {mod_name} ({module_path}): {e}")
else:
print("指定されたmodule_pathが.pyファイルかディレクトリではありません。")
return
# --------------------------
# 関数収集
# --------------------------
# label: "module.func"
items = []
for mod_name, mod_obj in modules_info:
for fname, fobj in inspect.getmembers(mod_obj, inspect.isfunction):
label = f"{mod_name}.{fname}"
items.append((label, mod_name, fname, fobj))
# 表示用にソート
items.sort(key=lambda x: x[0].lower())
if not items:
print("関数が見つかりませんでした。")
return
# モジュール一覧
module_names = sorted(set(mn for _, mn, _, _ in items), key=str.lower)
# --------------------------
# ウィジェット
# --------------------------
output = widgets.Output(layout=widgets.Layout(border="1px solid #ddd", padding="8px"))
module_dd = widgets.Dropdown(
options=["(all)"] + module_names,
value="(all)",
description="module:",
layout=widgets.Layout(width="100%")
)
search_box = widgets.Text(
value="",
placeholder="検索(関数名・モジュール名の一部)",
description="search:",
layout=widgets.Layout(width="100%")
)
# Dropdownは内部的にスクロールが効くので、ボタン大量より潰れにくい
func_dd = widgets.Dropdown(
options=[label for (label, _, _, _) in items],
description="func:",
layout=widgets.Layout(width="100%")
)
show_btn = widgets.Button(
description="Show doc",
button_style="",
layout=widgets.Layout(width="140px")
)
clear_btn = widgets.Button(
description="Clear",
button_style="",
layout=widgets.Layout(width="100px")
)
status = widgets.HTML(value="", layout=widgets.Layout(width="100%"))
def filtered_items():
mod_filter = module_dd.value
q = (search_box.value or "").strip().lower()
def ok(item):
label, mn, fn, _ = item
if mod_filter != "(all)" and mn != mod_filter:
return False
if q:
# label内(module.func)に対して部分一致
return q in label.lower()
return True
return [it for it in items if ok(it)]
def refresh_func_dropdown(*_):
its = filtered_items()
if not its:
func_dd.options = []
func_dd.value = None
status.value = "<span style='color:#b00'>該当する関数がありません。</span>"
return
labels = [it[0] for it in its]
current = func_dd.value
func_dd.options = labels
# 可能なら元の選択を維持
if current in labels:
func_dd.value = current
else:
func_dd.value = labels[0]
status.value = f"<span style='color:#555'>表示候補: {len(labels)} 件</span>"
def find_by_label(label):
for (lb, mn, fn, fo) in items:
if lb == label:
return mn, fn, fo
return None
def show_doc(*_):
label = func_dd.value
if not label:
return
found = find_by_label(label)
if not found:
return
mn, fn, fo = found
with output:
clear_output()
doc = fo.__doc__ or ""
doc = doc if doc.strip() else "(この関数にはドキュメンテーション文字列がありません)"
display(HTML(f"<h3>{mn}.{fn}</h3><pre style='white-space:pre-wrap'>{doc}</pre>"))
def clear_doc(*_):
with output:
clear_output()
module_dd.observe(refresh_func_dropdown, names="value")
search_box.observe(refresh_func_dropdown, names="value")
show_btn.on_click(show_doc)
clear_btn.on_click(clear_doc)
# 初期化
refresh_func_dropdown()
show_doc()
# 左パネル(固定高さで、要素を縦に配置)
left_panel = widgets.VBox(
[
module_dd,
search_box,
func_dd,
widgets.HBox([show_btn, clear_btn]),
status,
],
layout=widgets.Layout(
width=left_width,
height=height,
border="1px solid gray",
padding="8px",
justify_content="flex-start",
),
)
display(widgets.HBox([left_panel, output]))
# 使い方例:
# browse_user_functions("../src")
# browse_user_functions("../src", height="420px", left_width="520px")
この版は「大量のボタンを並べる」方式自体をやめているので、ボタンが潰れて見えにくくなる問題が起きません。必要なら、search_boxをTextではなくComboboxにする版(入力補完あり)にもできます。
user:
ありがとうございます。上記のコードで「module」「search」「func」の使い分けを教えてください。
assistant:
-
module:対象モジュール(ファイル/パッケージ階層)で絞り込むためのフィルタです。
-
"(all)":全モジュールを対象 - 例:
utils.mathを選ぶ →utils.math内の関数だけが候補になります - 「どのファイル(モジュール)にある関数か分かっている」場合に最短です。
-
-
search:文字列の部分一致でさらに絞り込むための検索欄です(
module.func全体に対して検索します)。- 例:
plotと入力 →visual.plot_lineやplot_histのように、名前にplotを含む関数だけが候補になります - 例:
math.sinと入れる →math.sinを含むものだけ、といった使い方もできます - 「関数名があいまい」「モジュール名もうろ覚え」でも探しやすいのが利点です。
- ※
moduleを"(all)"のままsearchだけで探すこともできます。
- 例:
-
func:絞り込まれた候補の中から、最終的に表示したい関数(docstringを見たい関数)を選ぶリストです。
-
moduleとsearchの結果で候補が更新され、その候補一覧がここに出ます。 -
Show docを押す(または初期表示)と、選択中のfuncのdocstringが右側に表示されます。
-
使い分けの典型例:
-
モジュールが分かっている →
moduleを選ぶ →funcから選ぶ(必要ならsearch) -
関数名の一部だけ覚えている →
module=(all)のままsearchに入力 →funcから選ぶ -
モジュールも関数名もあいまい → まず
searchを広めに入れる → ヒットが多ければmoduleで絞る →funcで決定
user:
ありがとうございました。以上の内容をブログにまとめたいので、タイトル案を考えてください。
assistant:
- Jupyterで関数一覧が「ボタンで潰れる」問題を解決:検索+Dropdownで快適にdocstring閲覧
- ipywidgetsで関数ブラウザを作る:大量ボタンをやめてフィルタ検索UIに改善
- Pythonモジュール内の関数を探しやすく:module/search/funcで絞り込む関数一覧ウィジェット
- Jupyter Notebook向け「関数ドキュメントブラウザ」自作メモ:ボタン地獄からの脱出
- os.walk+inspect+ipywidgetsで作る関数一覧ツール:検索でスッキリ表示
- 大量の関数でも見やすい:Jupyter用 関数リストUI(検索・モジュール絞り込み対応)
- 「関数が多すぎて見つからない」を解消:Jupyterでの関数探索ウィジェット改善
- Python関数のdocstringを一覧・検索:スクロール不要のJupyterウィジェット実装
- クリックできる関数一覧の作り方(Jupyter/ipywidgets):ボタン表示の限界と改善策
- Jupyterで自作ライブラリをブラウズする:関数一覧を検索してdocstring表示する方法
より寄せたい方向(初心者向け/実装メイン/UI改善メイン)や、ブログの読者層が分かれば、それに合わせてもう少し尖ったタイトルにもできます。