複数のPythonスクリプトを対象に、モジュールがimportされた回数を知りたくなりました。
ロードされているモジュールはsys.modules
などが使えますが、これではimportされた回数が分かりません。
調べてみたところ、標準ライブラリmodulefinder
+ collections.Counter
を使えば、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
こんな感じのディレクトリ・ファイルを用意します。
$ 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()があるだけです。
spam_package/spam_module.py
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)))
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']
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:
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))
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:
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()
Pythonスクリプトでimportしているのは、
- 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)]
は正しく数え上げられているようです。
GitHubに上げました。
thinkAmi-sandbox/python_modulefinder-sample