Python + modulefinder + collections.Counterで、モジュールがimportされた回数を調べる

複数の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

 

用意したモジュールや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()があるだけです。

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)))
        #=> 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 hamimport 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()

 
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