プログラミング/python の変更点

更新


#author("2026-07-03T09:07:25+00:00","default:administrator","administrator")
#author("2026-07-04T01:04:43+00:00","","")
[[公開メモ]]

* import の仕組み [#x9091d6d]

python では 

- module.py みたいな個々のファイルがモジュールと呼ばれ、
- モジュールを複数入れたフォルダに _ _init_ _.py という空のファイルを置くと、
そのフォルダはパッケージとして扱われる(pukiwikiで表示が崩れないよう _ と _ の間にスペースを入れてます。悪しからず。)
- import 時には package/module.py を package.module として参照可能

というのが基本なんだけど、驚くべきはここからで、

 LANG:python
 from some_package.some_module import xxx 

と書くとこれは感覚的には

 LANG:python
 from root.**.some_package.some_module import xxx

と書いたのと同じことになる。

つまり sys.path にある検索パスの配下のどこかで some_package.some_module が見つかれば、
「最初に見つかったもの」が import される(_ _init_ _.py によるパッケージ化を前提)。

これはどこで import したかにかかわらないので、~
package1/package2/package3/module.py から、~
package1/module.py を import pakcage1.module したりできる。

とにかく自分がどこにいるかは関係なく、つねに sys.path からの探索になる。

一方、パッケージ内では

 LANG:python
 from .some_file import xxx 

みたいな相対 import もできる。

ただしこれは自身がパッケージ名付きで import 
されていないとダメで、単体ファイルとして import された場合には「現在のモジュール」が空になってしまうため相対 import が

 ImportError: attempted relative import with no known parent package

という分かりにくいエラーで落ちます。

フォルダ構造とモジュール構造がほぼ1:1なのに完全に1:1じゃないのでとてもややこしい。

_ _init_ _.py が置かれているファイルの単体 import なんて需要ゼロだろうに、
どうしてこのまま放置されているのか全く理解できない。

しかもこういうあやうい仕様なのにベストプラクティスが周知されていないのもとても不思議。


* python import に関する覚書 [#w1d9191c]

初心者向けに、「罠」がそこらじゅうに敷き詰められてる。ほんと、えげつない。

- パッケージの独立性を保つにはパッケージ内の内部参照に相対 import を使うことが推奨される。
- 相対 import はパッケージ階層を前提にするので各フォルダに _ _init_ _.py を置いてパッケージとして明示する。
- パッケージ内のファイルをパッケージ名も相対指定もなしで import すると独立ファイル扱いになってしまうので、そのファイルからの相対 import が壊れてエラーになる。
-- 起動時も、python -m package.module としてパッケージ名付きで起動しないとダメ
--- __main__.py を置けば python -m package だけで __main__.py を起動できる
-- notebook の実行にはパッケージ名を指定できないので、notebookから相対 import はできない。
--- notebook から同階層にあるモジュールをパッケージ名付きで import するには、その **親フォルダ** が sys.path の下になければならない。
--- 大抵親フォルダは参照されないので、手動でこれを追加しないと import できなくてエラーになる

コメント:

- 相対 import じゃないと、どこかに同名のモジュールがあればバッティングする
- がちがちにパッケージ名付けるとポータビリティが失われる
- _ _init_ _.py 置かなくてもいい、と書かれた記事をよく見るけど、これも罠なので無視する
- パッケージ内のモジュールをパッケージ名なしで import すると破綻する。理由が分からずとても困る。
- notebook からの import も非常にややこしい

* プロファイリング [#t0ad56a0]

python は普通に書くと思った以上に遅いことになる場合が多いので、
プロファイリングの結果を基に重い処理を numpy 側に移したりするのが役に立つ

いろいろ試した結果、特に並列処理を使ったバッチ処理が走るライブラリを使う場合には
jupyter 上でのプロファイリングするとライブラリ関数の実行中に UI スレッドが動いて
プロファイル結果が無茶苦茶になるようです。

したがって面倒でもプロファイルは Jupyter 外で行った方が間違いない。

** 前処理結果を dump する [#j33e19a7]

jupyter 上で計算した結果を引き継ぎつつ
jupyter 外で処理を回すには、前処理で得られたデータ(プロファイリングしたい処理への入力)を
pickle.dump しておき、外部で動かす処理ではそれを呼んで動作を行う

Jupyter セル内で実行
 LANG:python
 import pickle
 payload = {
     "param1": param1,
     "param2": param2,
     "param3": param3,
 }
 with open("payload.pkl", "wb") as f:
     pickle.dump(payload, f, protocol=pickle.HIGHEST?PROTOCOL)

で(通常の python データであれば)書き出せる

** 読み込んで動かす .py ファイル [#gd0caee2]

do_profile.py
 LANG:python
 import pickle
 import cProfile
 import pstats
 
 from my_module import func_to_profile
 
 def main():
     with open("payload.pkl", "rb") as f:
         payload = pickle.load(f)
 
     prof = cProfile.Profile()
     prof.enable()
     refined = func_to_profile(**payload)
     prof.disable()
 
     prof.dump_stats("prof.out")
 
 if __name__ == "__main__":
     main()

** 実行する [#d467ea3d]

 LANG:console
 $ python do_profile.py

** 解析する [#s2942399]

 LANG:console
 $ python -m pstats prof.out
 Welcome to the profile statistics browser.
 prof.out% strip        # strip directory names
 prof.out% sort cumtime # sort by cumtime
 prof.out% stats 40     # show first 40 items

これで、

my_module.py:1234(my_function)

みたいな関数ごとに

 LANG:console
 ncalls   tottime  percall  cumtime  percall filename:lineno(function)
 呼び出し 自前処理 呼び出し 子も含む 呼び出し ファイル名:行番号(関数名)
 回数     時間     あたり   全時間   あたり

のリストが出ます

特定の関数内から呼ばれている分だけを調べたければ、

 LANG:console
 prof.out% sort time
 prof.out% callees my_module.py:1234(my_function)

などとします。

* ipynb 向けのスニペット [#n2126fb8]

** 必要なモジュールを確実に見つけられるように検索パスを広げる [#yf8e132f]

 LANG:python
 from pathlib import Path
 import sys
 
 # 必要なモジュールを確実に見つけられるように検索パスを広げる
 #
 # from some_module.some_file import xxx は実質的に
 # from root.**.some_module.some_file import xxx のように働くので、
 # root の配下のどこかに some_module があれば確実に import できる
 # しかし some_module と同列に置かれた ipynb では root が
 # some_module になって、その下で some_module を探してエラーになる
 # ここは root を十分上に持っていくための処理
 
 # どれかが見つかった場所をルート扱いにする
 MARKERS = ("pyproject.toml", ".git", ".vscode")
 p = Path.cwd().resolve()
 root = None
 for parent in (p, *p.parents):
     if any((parent / m).exists() for m in MARKERS):
         root = parent
         break
 if root is None:
     # 見つからなければさしあたり親ディレクトリにしておく
     root = Path.cwd().resolve().parents[0]
 
 # 先頭がrootでなければ、既存のrootエントリを全部消してから先頭に入れる
 print(f"Adding this folder to sys.path: {root}")
 root_s = str(root)
 if not sys.path or sys.path[0] != root_s:
     sys.path[:] = [p for p in sys.path if p != root_s]
     sys.path.insert(0, root_s)

** オートリロード [#pef8f02b]

ソースファイルが日本語を含む可能性があるならこれをまず実行

 LANG: python
 import builtins
 
 # デフォルトでファイルは UTF-8 で読むように open を置き換える
 # 日本語コメント入りの .py ファイルを autoreload しようとして落ちるのを避けるため
 if "_original_open" not in globals():
     _original_open = builtins.open
 
     def open_utf8_default(path, mode="r", *args, **kwargs):
         # binary モードや明示指定がある場合は素通し
         if "b" not in mode and "encoding" not in kwargs:
             kwargs["encoding"] = "utf-8"
         return _original_open(path, mode, *args, **kwargs)
 
     builtins.open = open_utf8_default

で、これが自動リロードの設定。

 LANG: python
 # 自動リロードを設定
 %reload_ext autoreload
 %autoreload 2

** 必要に応じて依存モジュールを pip install する [#g496960a]

 LANG:python
 ensure_package("trimesh")

みたいにしてモジュールがない場合に限って pip install できるようになる。

 LANG:python
 # 必要に応じて必要なモジュールをインストールする
 import sys
 import subprocess
 import importlib.util
 
 print(sys.executable)
 print(f"sys.version: {sys.version}")
 
 def _get_installed_version(dist_name: str) -> str | None:
     """
     Return installed distribution version (e.g., 'numpy' -> '1.26.4').
     Prefer importlib.metadata; fallback to pkg_resources for older Pythons.
     """
     try:
         # Python 3.8+
         from importlib.metadata import version, PackageNotFoundError  # type: ignore
         try:
             return version(dist_name)
         except PackageNotFoundError:
             return None
     except Exception:
         # Older Python or environment without importlib.metadata
         try:
             import pkg_resources  # type: ignore
             try:
                 return pkg_resources.get_distribution(dist_name).version
             except Exception:
                 return None
         except Exception:
             return None
 
 def _get_module_dunder_version(import_name: str) -> str | None:
     """Try module.__version__ as a last resort."""
     try:
         import importlib
         m = importlib.import_module(import_name)
         v = getattr(m, "__version__", None)
         return v if isinstance(v, str) and v else None
     except Exception:
         return None
 
 def ensure_package(import_name: str, dist_name: str | None = None, pip_args: list[str] | None = None) -> None:
     """
     Ensure a module is importable. If not, install it via pip.
     - import_name: the name used in `import ...`
     - dist_name  : the name used in `pip install ...` / metadata lookup (often same)
     """
     dist = dist_name or import_name
 
     if importlib.util.find_spec(import_name) is None:
         print(f"{import_name} not found. Installing {dist}...")
         args = [sys.executable, "-m", "pip", "install", dist]
         if pip_args:
             args.extend(pip_args)
         subprocess.check_call(args)
         # install後の表示(取れなければunknown)
         ver = _get_installed_version(dist) or _get_module_dunder_version(import_name) or "unknown"
         print(f"{import_name} installed. version={ver}")
     else:
         ver = _get_installed_version(dist) or _get_module_dunder_version(import_name) or "unknown"
         print(f"{import_name} already importable. version={ver}")
 
 # ここに必要なパッケージを並べる
 ensure_package("scipy")
 ensure_package("trimesh")
 ensure_package("embreex") # for raycast
 ensure_package("fast_simplification", dist_name = "fast-simplification")

* vscode 下で uv で python 使うおまじない [#i78816da]

 $ cat >pyproject.toml
 # 必要ない lint エラーを非表示
 [tool.ruff.lint]
 ignore = ["E741", "E731"]
 
 # バージョン指定
 [dependency-groups]
 dev = [
     "ipykernel>=7.2.0",
     "nbconvert>=7.17.1",
     "pytest>=9.1.1",
 ]
 
 # 新しすぎるパッケージは入れない
 [tool.uv]
 exclude-newer = "1 week"
 
 $ mkdir .claude
 $ cat >.claude/settings.local.json
 {
   "permissions": {
     "allow": [
       "Bash(ls:*)",
       "Bash(wc:*)",
       "Bash(uv run pytest tests/:*)",
       "Bash(uv run jupyter nbconvert:*)"
     ]
   }
 }
 $ cat >CLAUDE.md
 ## 開発環境
 
 - パッケージ管理は `uv`(依存追加は `uv add` / `uv add --dev`)。
 - 権限の allow ルール(`Bash(uv run pytest tests/:*)` など)は個人用の `.claude/settings.local.json` に置く(リポジトリには含めない)。
 
 ### テスト・notebook の実行規約
 
 - **`.ipynb` の中身を確認するときは Read ツールを使う** (`uv run python -c "import json..."` のように無駄に Python を起動すると追加で権限確認が出る)。
 - **テストは `uv run pytest tests/...`** と `tests/` パスを付けて実行する。
 - **notebook 実行は `uv run jupyter nbconvert --to notebook --execute --inplace <ファイル>`**(`nbconvert` は dev 依存に追加済み。`--with` は不要)。
 - `.ipynb` を編集する際は、Read から NotebookEdit までの間にファイルが変わらないようにする(IDE のオートセーブ/linter で `File has been modified since read` になる)。編集直前に Read し直す。編集対象の notebook はエディタで閉じておくと安全。
 $ cat >.gitignore
 __pycache__/
 *.pyc
 *.pyo
 *.pyd
 env/
 output/
 
 $ git init
 $ uv python list     # 入手可能な python のバージョンを確認
 $ uv python pin 3.14 # 適当なものを選んで固定
 $ uv sync --dev      # 仮想環境と依存を同期
                      # Notebook カーネルを登録
 $ uv run python -m ipykernel install --user --name tmdc-band --display-name "Python (tmdc-band)"
 $ uv run python -m ipykernel install --user --name <プロジェクト名> --display-name "Python (プロジェクト名)"

そのままだと .venv 環境が vscode に認識されないことがあるので、
必要に応じて Reload Window すると、.py や .ipynb で .venv/Scripts/python.exe をインタプリタとして開発を始められる。

Counter: 680 (from 2010/06/03), today: 1, yesterday: 9