ローカルスコープの外に飛び出すオブジェクト?

ローカルスコープの外に飛び出すオブジェクト?

Python の深い話?

 久しぶりのプログラミング日記です。
 BlogCat がその日の気分で書きたいことを書くカテゴリの記事です。
 普段はプログラミングとは全く関係ないこと(主に食べ物の話)も書いたりしますが、ごく稀に一般記事では書かないような、Python のディープな機能や特性を書いたりするので、時々覗いてみると思わぬ掘り出し物があるかもしれません。

関数内部でモジュールをインポート

 def文を使って関数を定義するときに、関数ブロックでインポートしたモジュールは関数内部に限って有効です。たとえば、

def inner_sin(x):
    import math
    return math.sin(x)

のような関数を定義して、

inner_sin(0)

を実行すると「 0.0 」という値が返りますが、

math.sin(0)

を実行しても NameError ("そんな名前の関数はありませんよ"というエラー) が返ります。関数の外側ではモジュールがインポートされていないからです。

 つまり、関数内部でインポートされた関数も ローカル変数 と似たようなスコープのルールが適用されます。このようなルールがあるので、関数内部でモジュールのインポートを実行しておけば、ユーザーがその関数を利用するための環境に気をとらわれずに済むという利点があります。
 

ローカルスコープの外に飛び出すオブジェクト?

 もう少し深く調べていくと、関数内部のインポートが面白い性質を示すことに気づきます。たとえば、次のような関数を定義します。

def inner_sqrt(x):
    import numpy as np
    return np.sqrt(x)

 np.sqrt(x) は配列 (ndarray) を受け取って各要素の平方根を返す関数です。inner_sqrt() は関数内部で numpy をインポートしているので、関数の外で配列を生成することはできません:

a = np.array([1, 2, 3])
NameError: name 'np' is not defined

 しかし、inner_sqrt() にリストを渡すと、ちゃんと配列が返ってくるのです:

inner_sqrt([1, 2, 3])
array([1.        , 1.41421356, 1.73205081])

 本来ならば関数の外に存在しないはずの NumPy 特有のオブジェクトがローカルスコープを抜けて外に出てきています。NumPy 配列の演算機能もちゃんと使えます:

f = inner_sqrt([2, 4])
g = inner_sqrt([9, 16])

print(f + g)
[4.41421356 6.        ]

 つまり、関数の戻り値として設定しておけば、インポートもしていないのに、モジュール特有のオブジェクトは使えるという不思議な状態が実現するのです。昔のファミコンの裏技(← 古い)みたいで面白いですね。