Python

Reloadしたモジュールについての注意点

インポートしたモジュールのソースを変更して、その変更を実行しているセッションに反映したいとき、モジュールを importlib.reload() (Python3) でリロード出来ますが、

  • リロードした前後のモジュールは同一オブジェクトですが、
  • リロードした前後のモジュールで定義されていた関数は別オブジェクト
  • リロードする前の関数が実行されるグローバル変数は更新後

となる点に注意が必要です。以下、サンプル例です。

更新前が次のコードだったとします。

reloadtest.py
def foo(n):
    """Return 0 for all n >= 0"""
    if n == 0:
        return n
    else:
        return foo(n - 1)

def baz(n):
    """Return True"""
    return True

インタラクティブセッションで、次のようにfooを調べておきます。

>>> import reloadtest
>>> dir(reloadtest)
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'baz',
 'foo']

>>> foo = reloadtest.foo
>>> baz = reloadtest.baz
>>> foo(0)
0
>>> foo(2)
0

次に、先ほどのモジュールを更新して、下記の変更をくわえます。

reloadtest.py
def foo(n):
    """Return 1 for all n >= 1"""
    if n == 1:
        return n
    else:
        return foo(n - 1)


def bar(n):
    """Return 2 for all n >= 0"""
    if n == 2:
        return n
    else:
        return foo(n)

次からは、モジュールのリロードとその後の挙動を調べたセッションです。
更新前後のモジュールオブジェクトは同一であることが次から分かります。

>>> from importlib import reload
>>> newmodule = reload(reloadtest)
>>> newmodule is reloadtest
True

reloadtestモジュールのグローバルには、更新後のソースにはないbazが残っていることが分かります。

>>> dir(reloadtest)
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'bar',
 'baz',
 'foo']

更新前後のfooは、異なる関数オブジェクトであることが分かります。

>>> foo is reloadtest.foo
False

最後のポイントですが、古いfooを実行すると、古いコードオブジェクトが実行されていますが、
foo(2)は、新しい関数の結果です。つまり、古い関数の中のfooは、新しいfoo
を指していることがわかります。

>>> foo(1)
0
>>> foo(2)
1

特にinspectライブラリのを用いてgetsourceなど、オブジェクトのソースを取得する関数は、オブジェクト自体にはソースは保存されておらず、呼ばれた時点で保存されているファイル名と行番号を頼りにソースファイルを見に行くためリロード前に定義されていた古いオブジェクトに使用すると、更新後ソースを見に行き、オブジェクトに保存されているcode objectとは対応しなくなります。

__main__foobazは、ともに更新前の古い関数ですが、fooは更新後のソース
bazは、更新前にあった場所に定義された別の関数barをとってきてしまっています。

>>> import inspect
>>> print(inspect.getsource(foo))
def foo(n):
    """Return 1 for all n >= 0"""
    if n == 1:
        return n
    else:
        return foo(n - 1)
>>> print(inspect.getsource(baz))
def bar(n):
    """Return 2 for all n >= 0"""
    if n == 2:
        return n
    else:
        return foo(n)