複数のPythonスクリプトを対象に、モジュールがimportされた回数を知りたくなりました。
ロードされているモジュールはsys.modules
などが使えますが、これではimportされた回数が分かりません。
調べてみたところ、標準ライブラリmodulefinder
+ collections.Counter
を使えば、importされた回数がわかりそうだったため、その時のメモを残します。
- 31.3. modulefinder — スクリプト中で使われているモジュールを検索する — Python 3.6.1 ドキュメント
- 8.3. collections — コンテナデータ型 — Python 3.6.1 ドキュメント
目次
- 環境
- 用意したモジュールやPythonスクリプト
- ModuleFinderの属性
- ModuleFinderの中身
- 複数のファイルに対してrun_script()する時の注意点
- importとfrom~importの違い
- モジュールがimportされた回数を調べる
- ソースコード
環境
pyenvのupgrade
本題とは関係ありませんが、Python3.6.1がリリースされたため、pyenvをupgradeしてインストールしました。
# 現在のpyenvのバージョンを確認 $ pyenv --version pyenv 1.0.7 # インストールできるPythonのバージョンを確認 $ pyenv install --list Available versions: ... 3.6.0 3.6-dev 3.7-dev # brewでpyenvをアップグレード $ brew upgrade pyenv ... 🍺 /usr/local/Cellar/pyenv/1.0.10: 560 files, 2.2MB, built in 12 seconds # 再度確認 $ pyenv --version pyenv 1.0.10 $ pyenv install --list Available versions: ... 3.6.0 3.6-dev 3.6.1 3.7-dev # インストール $ pyenv install 3.6.1 ... Installed Python-3.6.1 to /Users/kamijoshinya/.pyenv/versions/3.6.1 # インストールされているバージョンを確認 $ pyenv versions system * 3.6.0 3.6.1 # Pythonのバージョンを切り替え $ pyenv global 3.6.1 # Pythonのバージョンを確認 $ python --version Python 3.6.1
用意したモジュールやPythonスクリプト
こんな感じのディレクトリ・ファイルを用意します。
$ tree . ├── from_import.py ├── from_import_ham_only.py ├── from_import_spam_only.py ├── import.py ├── eggs_package │ ├── __init__.py │ └── eggs_module.py ├── ham_package │ ├── __init__.py │ └── ham_module.py └── spam_package ├── __init__.py └── spam_module.py
各モジュールにはprint()があるだけです。
def spam(): print('spam')
ham_package/ham_module.py
def ham(): print('ham')
eggs_package/eggs_module.py
def eggs(): print('eggs')
上記のモジュールをimportするPythonスクリプトはこんな感じです。
import.py
import ham_package.ham_module import eggs_package.eggs_module import spam_package.spam_module ham_package.ham_module.ham() eggs_package.eggs_module.eggs() spam_package.spam_module.spam()
from_import.py
from ham_package.ham_module import ham from eggs_package.eggs_module import eggs from spam_package.spam_module import spam ham() eggs() spam()
from_import_spam_only.py
from spam_package.spam_module import spam
from_import_ham_only.py
from ham_package.ham_module import ham
ModuleFinderの属性
ModuleFinderオブジェクトやModuleオブジェクトの属性を調べてみました。
finder = ModuleFinder() finder.run_script('from_import.py') print('dir ModuleFinder: {}'.format(dir(finder))) for name, mod in finder.modules.items(): print('type:{}'.format(type(mod))) #=> type:<class 'modulefinder.Module'> print('dir Module object:{}'.format(dir(mod)))
実行結果
# ModuleFinderオブジェクトの属性 dir ModuleFinder: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_add_badmodule', '_safe_import_hook', 'add_module', 'any_missing', 'any_missing_maybe', 'badmodules', 'debug', 'determine_parent', 'ensure_fromlist', 'excludes', 'find_all_submodules', 'find_head_package', 'find_module', 'import_hook', 'import_module', 'indent', 'load_file', 'load_module', 'load_package', 'load_tail', 'modules', 'msg', 'msgin', 'msgout', 'path', 'processed_paths', 'replace_paths', 'replace_paths_in_code', 'report', 'run_script', 'scan_code', 'scan_opcodes'] # Moduleオブジェクトの属性 dir:['__class__', '__code__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__file__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__path__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'globalnames', 'starimports']
ModuleFinderの中身
ModuleFinder.run_script()後の属性値を見てみます。
finder = ModuleFinder() finder.run_script('from_import_ham_only.py') for name, mod in finder.modules.items(): print('-'*10) print('name:{}'.format(name)) print('globalnames:{}'.format(mod.globalnames)) print('modules:{}'.format(','.join(list(mod.globalnames.keys())))) print('starimports:{}'.format(mod.starimports)) print('bad modules:{}'.format(','.join(finder.badmodules.keys())))
実行結果
---------- name:__main__ globalnames:{'ham': 1} modules:ham starimports:{} ---------- name:ham_package globalnames:{} modules: starimports:{} ---------- name:ham_package.ham_module globalnames:{'ham': 1} modules:ham starimports:{} bad modules:
複数のファイルに対してrun_script()する時の注意点
ModuleFinderオブジェクトを使い回し、複数ファイルに対して、ModuleFinder.run_script()してみます。
files = ['from_import_spam_only.py', 'from_import_ham_only.py'] # ModuleFinderオブジェクトを使いまわす finder = ModuleFinder() for f in files: finder.run_script(f) for name, mod in finder.modules.items(): print('-'*10) print('file:{}'.format(f)) print('name:{}'.format(name)) print('modules:{}'.format(','.join(list(mod.globalnames.keys()))))
実行結果を見ると、from_import_ham_only.py
はimportしているham
の他、spam
も含まれていました。
---------- file:from_import_spam_only.py name:__main__ modules:spam ---------- file:from_import_spam_only.py name:spam_package modules: ---------- file:from_import_spam_only.py name:spam_package.spam_module modules:spam ---------- file:from_import_ham_only.py name:__main__ modules:spam,ham <= hamだけなのにspamがいる ---------- file:from_import_ham_only.py name:spam_package modules: ---------- file:from_import_ham_only.py name:spam_package.spam_module modules:spam ---------- file:from_import_ham_only.py name:ham_package modules: ---------- file:from_import_ham_only.py name:ham_package.ham_module modules:ham
そのため、ModuleFinderオブジェクトを使いまわさずに、
files = ['from_import_spam_only.py', 'from_import_ham_only.py'] for f in files: # ModuleFinderオブジェクトは、ファイルごとに生成する finder = ModuleFinder() finder.run_script(f) for name, mod in finder.modules.items(): print('-'*10) print('file:{}'.format(f)) print('name:{}'.format(name)) print('modules:{}'.format(','.join(list(mod.globalnames.keys()))))
としたところ、正しい結果が出ました。
---------- file:from_import_spam_only.py name:__main__ modules:spam ---------- file:from_import_spam_only.py name:spam_package modules: ---------- file:from_import_spam_only.py name:spam_package.spam_module modules:spam ---------- file:from_import_ham_only.py name:__main__ modules:ham <= hamだけになった ---------- file:from_import_ham_only.py name:ham_package modules: ---------- file:from_import_ham_only.py name:ham_package.ham_module modules:ham
importとfrom~importの違い
from ham_package.ham_module import ham
とimport ham_package.ham_module
で違いがあるかをみてみます。
files = ['from_import.py', 'import.py'] for f in files: print('='*10) print('filename:{}'.format(f)) # ModuleFinderオブジェクトは、ファイルごとに生成する finder = ModuleFinder() finder.run_script(f) for name, mod in finder.modules.items(): print('-'*10) print('name:{}'.format(name)) print('modules:{}'.format(','.join(list(mod.globalnames.keys()))))
__main__
以外の結果は同じようです。
========== filename:from_import.py ---------- name:__main__ modules:ham,eggs,spam ---------- name:ham_package modules: ---------- name:ham_package.ham_module modules:ham ---------- name:eggs_package modules: ---------- name:eggs_package.eggs_module modules:eggs ---------- name:spam_package modules: ---------- name:spam_package.spam_module modules:spam ========== filename:import.py ---------- name:__main__ modules:ham_package,eggs_package,spam_package ---------- name:ham_package modules: ---------- name:ham_package.ham_module modules:ham ---------- name:eggs_package modules: ---------- name:eggs_package.eggs_module modules:eggs ---------- name:spam_package modules: ---------- name:spam_package.spam_module modules:spam
モジュールがimportされた回数を調べる
ここからが本題ですが、ModuleFinderとcollections.Counterを使って、モジュールがimportされた回数を調べてみます。
from modulefinder import ModuleFinder import os from collections import Counter def is_target(filename): if '__' in filename: # __file__や__init__.pyを除外 return False if 'report' in filename: return False if os.path.splitext(filename)[1] != '.py': return False return True def collect_files(): root_dir = os.path.abspath(os.path.dirname(__file__)) results = [] for root, dirs, files in os.walk(root_dir): dirs[:] = [d for d in dirs if 'env' not in os.path.join(root, d)] targets = [os.path.join(root, f) for f in files if is_target(f)] results.extend(targets) return results def main(): files = collect_files() modules = [] for f in files: finder = ModuleFinder() finder.run_script(f) for name, mod in finder.modules.items(): if name == '__main__': continue if not mod.globalnames.keys(): continue modules.append(name) c = Counter(modules) print(c.most_common()) if __name__ == '__main__': main()
- import.py (spam, ham, eggs)
- from_import.py (spam, ham, eggs)
- from_import_ham_only.py (ham)
- from_import_spam_only.py (spam)
なので、実行結果の
[('ham_package.ham_module', 3), ('spam_package.spam_module', 3), ('eggs_package.eggs_module', 2)]
は正しく数え上げられているようです。