LoginSignup
3
4

More than 1 year has passed since last update.

IPythonでimport文を省略したいときの最適解

Last updated at Posted at 2022-09-21

はじめに

ターミナル上で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しているので、当然起動が遅くなります。特にpandasmatplotlibはかなり重いので、起動がだいぶ遅れます。

できれば、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

各行を説明すると:

  1. import_moduleは引数にモジュールを文字列で渡してimportされたモジュールオブジェクトを返します。self._name"numpy"ならmodnumpyが入り、mod.arrayとかが使える状態です。
  2. get_ipython()は現在起動中のIPythonシェルを返します。ターミナル内でIPythonを起動している場合とJupyter QtConsoleを起動している場合では異なるオブジェクトが返されますが、当然共通のメソッドが実装されています。
  3. 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
3
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4