はじめに
ターミナル上でIPythonを実行したりJupyter QtConsole使ったりというときに、毎回import matplotlib.pyplot as plt
と打ったり、NameError: name 'plt' is not defined
でイラついたりを避けたいですよね。
よく見る解決策
IPython
は起動時にユーザーディレクトリからたどって /.ipython/profile_default/startupディレクトリ直下にあるpyファイルを全部(多分)実行します。この中にたとえば00-startup.pyファイルを
# 00-startup.py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
で保存しておくと、IPythonの起動後すぐにnp
とか使えます
欠点
起動時にimportしているので、当然起動が遅くなります。特にpandas
やmatplotlib
はかなり重いので、起動がだいぶ遅れます。
できれば、plt
を使わないときはimportせず、使うときは使うときでわざわざimportせずにどうにかしてほしいです。
最適解!
00-startup.pyに次のLazyImporter
を定義します(少しずつ解説します)。
## 00-startup.py
class LazyImporter:
def __init__(self, name: str, alias: str = None):
self._name = name
self._alias = alias or name
def __repr__(self):
return repr(self._mod)
def __str__(self):
return str(self._mod)
def __getattr__(self, name):
return getattr(self._mod, name)
@property
def _mod(self):
from IPython import get_ipython
import importlib
mod = importlib.import_module(self._name)
shell = get_ipython()
shell.push({self._alias: mod}) # update namespace
return mod
何がやりたいかというと、np = LazyImporter("numpy", "np")
で初期化し、np.array
とか呼び出したときにnumpy
をimportしてグローバルの名前空間のnp
を上書きするという作戦です。ポイントは_mod
の中身です。
class LazyImporter:
...
@property
def _mod(self):
from IPython import get_ipython
import importlib
mod = importlib.import_module(self._name) # (1)
shell = get_ipython() # (2)
shell.push({self._alias: mod}) # (3)
return mod
各行を説明すると:
-
import_module
は引数にモジュールを文字列で渡してimportされたモジュールオブジェクトを返します。self._name
が"numpy"
ならmod
にnumpy
が入り、mod.array
とかが使える状態です。 -
get_ipython()
は現在起動中のIPythonシェルを返します。ターミナル内でIPythonを起動している場合とJupyter QtConsoleを起動している場合では異なるオブジェクトが返されますが、当然共通のメソッドが実装されています。 -
push
メソッドはシェルのグローバルな名前空間をdict
で更新します。ここで{"np": numpy}
を渡せばnp
が使えるようになるわけです。
あとは、モジュールが必要になる場面でこの_mod
プロパティを呼び出すように細工すればよいです。考えられる場面はほぼnp.array
とかのようにモジュール内の関数やクラスにアクセスするときなので、__getattr__
で_mod
を呼び出し、名前空間を更新しつつモジュールの__getattr__
を呼べばよいです。
class LazyImporter:
...
def __getattr__(self, name):
return getattr(self._mod, name)
一応、>>> np
とか>>> print(np)
にも対応させるため、__repr__
も実装しておいたのが上の例になります。
こうすれば、使う可能性のあるモジュールを
## 00-startup.py
np = LazyImporter('numpy', 'np')
plt = LazyImporter('matplotlib.pyplot', 'plt')
pd = LazyImporter('pandas', 'pd')
sns = LazyImporter('seaborn', 'sns')
da = LazyImporter('dask.array', 'da'))
みたく片っ端から名前空間にぶち込んでも起動は早いままですね!
残る問題点
さすがに変なことをしているので、問題は残ります。例えば、numpy
がimportされていない状態でnp.a
とまで打ってTabで予測変換をしようとすると落ちます。これが回避できるかはもう少し試してみます。
2022/09/23 追記
import lockのせいでimport_module
で例外が生じているんだろうなと思ってはいたんですが、try/exceptだけでどうやら落ちるのは回避できるっぽいです。
class LazyImporter:
...
@property
def _mod(self):
from IPython import get_ipython
import importlib
try:
mod = importlib.import_module(self._name)
except Exception:
return
shell = get_ipython()
shell.push({self._alias: mod})
return mod