はじめに
自作の iTunes 登録ツール(Python + pywin32 で書いて exe にまとめたもの)が、ある日突然プレイリストへの曲追加で落ちるようになりました。コードは一切変えていません。
原因は win32com の gen_py キャッシュ でした。しかもそのキャッシュを作ったのは、同じ PC で動かした「値を読むだけ」の確認用スクリプトです。この記事では、症状の見分け方・仕組み・直し方を整理します。
症状
AttributeError: '<win32com.gen_py.(型ライブラリ名).IITPlaylist instance at 0x...>' object has no attribute 'AddFile'
次の2つがそろっていたら、コードより先にキャッシュを疑ってください。
- エラーメッセージに
win32com.gen_pyが含まれている - 同じ呼び出しが昨日までは通っていた
なぜ起きるのか
pywin32 で COM オブジェクトを扱う方法は2つあります。
| 方式 | 動き | 呼べるメソッド |
|---|---|---|
| 遅延バインディング(動的) | 呼び出しのたびに実体へ問い合わせる | 実体が持っていれば何でも |
| 事前バインディング(型付き) | 型ライブラリから生成したクラスを使う | 宣言された型のメソッドだけ |
win32com.client.Dispatch() はこの2つを実行時に自動で切り替えます。対象の型について生成済みクラスが gen_py キャッシュにあれば型付き版を、無ければ動的版を返します。つまり、同じコードでもキャッシュの有無で戻り値の性質が変わります。
私のケースでは、iTunes の Playlists.ItemByName() の戻り値が型の上では基本型 IITPlaylist と宣言されていました。AddFile は派生型 IITUserPlaylist のメソッドです。
- 動的なとき: 実体はユーザー作成プレイリストなので
AddFileが呼べる - 型付きになったとき: 「
IITPlaylistにAddFileは無い」と判定されてAttributeError
キャッシュを作る呼び出し
win32com.client.gencache.EnsureDispatch()-
win32com.client.CastTo()(対象が型付きでなければ内部でEnsureDispatchを呼ぶ) - makepy の実行
特に CastTo は要注意です。1つのオブジェクトの型を読み替えるだけに見えて、実際には型ライブラリ全体のキャッシュをディスクへ書き出します。私の確認用スクリプトも、曲の保存場所を読むために CastTo を1回呼んでいただけでした。
キャッシュの場所はパッケージ内に gen_py フォルダが無ければ %TEMP%\gen_py\<Pythonバージョン> です。同じユーザー・同じ Python バージョンのプログラム同士で共有されるため、別のスクリプトが作ったキャッシュを exe 化したツールが拾う、ということが起きます。
import win32com
print(win32com.__gen_path__) # 実際の場所を確認
いまどちらで動いているかを確認する
pl = itunes.LibrarySource.Playlists.ItemByName("テスト")
print(type(pl).__module__) # win32com.gen_py を含めば型付き
print(repr(pl)) # <COMObject ...> なら動的
「昨日と今日で挙動が違う」ときは、この出力を両方の状態で記録しておくと切り分けが早くなります。
対処
1. 動的な扱いに固定する(おすすめ)
import win32com.client.dynamic
itunes = win32com.client.dynamic.Dispatch("iTunes.Application")
dynamic.Dispatch はキャッシュを見ません。子オブジェクトも動的なまま返るので、後から誰かがキャッシュを作っても挙動が変わりません。代わりに win32com.client.constants や補完は使えないので、定数は自前で定義します。
2. 型付きでも動的でも動くように書く
既存コードの Dispatch を残したい場合は、メソッドの有無を見てから読み替えます。
def as_user_playlist(pl):
if hasattr(pl, "AddFile"):
return pl
return win32com.client.CastTo(pl, "IITUserPlaylist")
戻り値が基本型で宣言されているメソッドは他にもあることが多いので(私のツールでは曲オブジェクトも同様でした)、1か所で終わらせず洗い出してください。なお動的版での hasattr は COM への問い合わせが1回発生するため、ループ内では判定結果を使い回すと速くなります。
3. キャッシュを消す(応急処置)
import shutil, win32com
shutil.rmtree(win32com.__gen_path__)
module 'win32com.gen_py...' has no attribute 'CLSIDToClassMap' のような「壊れたキャッシュ」のエラーもこれで直ります。ただし誰かが再び EnsureDispatch / CastTo を呼べば元に戻るので、根本対策は1か2です。
教訓:確認用スクリプトこそ注意
「読むだけだから何も変えない」と思っていたスクリプトが、共有ディレクトリへ書き込みをしていました。
- 確認用スクリプトでは
CastTo・gencache・EnsureDispatchを使わず、動的版から値を辿る - 使ってしまったら、同じ COM を使う他のプログラムが動くか確認する
- 他人のツールがある PC で試す前に、
gen_pyフォルダの有無を記録しておく
まとめ
| 項目 | 内容 |
|---|---|
| 症状 | 昨日まで呼べたメソッドが AttributeError。メッセージに win32com.gen_py
|
| 原因 |
Dispatch がキャッシュの有無で型付き/動的を切り替える |
| キャッシュを作るもの |
EnsureDispatch・CastTo・makepy(同じ Python バージョン間で共有) |
| 見分け方 | type(obj).__module__ |
| 対処 |
dynamic.Dispatch で固定/hasattr+CastTo/gen_py 削除(応急) |
この件の経緯(AI に調査を手伝ってもらった話を含む)は元記事にも書いています。
https://kujiragames.com/2026/09/pywin32-gen-py-cache/