ラベル import の投稿を表示しています。 すべての投稿を表示
ラベル import の投稿を表示しています。 すべての投稿を表示

2016年1月10日

ベンチマーク11 lazy import

lazy import の3つのやり方について、繰り返し import 箇所を呼んだ場合の速度を計測します。


まず、ループの中で math.pi を一度だけ呼び出す処理のベンチマークを計測します。
ベンチマークのソースコードです。

from benchmarker import Benchmarker

# グローバル変数と専用クラスは先に定義
g_math = __import__('math')
c_math = LazyImport('math')

with Benchmarker(1000000, cycle=3, extra=1) as bench:

    @bench("local")
    def _(bm):
        for _ in bm:
            import math
            math.pi

    @bench("global")
    def _(bm):
        for _ in bm:
            global g_math
            if g_math is None:
                g_math = __import__('math')
            g_math.pi

    @bench("class")
    def _(bm):
        for _ in bm:
            c_math.pi

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking     real
global       0.5394  (100.0) ********************
local        5.1763  ( 10.4) **
class        5.2870  ( 10.2) **
計測結果からは、グローバル変数式が圧倒的に高速となりました。
この計測では、ローカルimport と LazyImportクラスはほぼ等速です。


続けて、ループの中で math.pi を2回呼び出す処理のベンチマークを計測します。
ベンチマークのソースコードです。

from benchmarker import Benchmarker

# グローバル変数と専用クラスは先に定義
g_math = __import__('math')
c_math = LazyImport('math')

with Benchmarker(1000000, cycle=3, extra=1) as bench:

    @bench("local")
    def _(bm):
        for _ in bm:
            import math
            math.pi
            math.pi

    @bench("global")
    def _(bm):
        for _ in bm:
            global g_math
            if g_math is None:
                g_math = __import__('math')
            g_math.pi
            g_math.pi

    @bench("class")
    def _(bm):
        for _ in bm:
            c_math.pi
            c_math.pi

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking     real
global       0.7084  (100.0) ********************
local        5.4640  ( 13.0) ***
class       10.2182  (  6.9) *
グローバル変数式が相変わらず速いです。
そして、LazyImportクラスは如実に遅くなりました。これは、LazyImportクラスは参照される度に getattr() を呼ぶことが原因です。


結論として、以下の2つが言えます。

  • import は2回目以降でもそれなりに重い処理である。よって、import 箇所に繰り返し通る可能性があるなら、グローバル変数式を使うべき。
  • LazyImportクラスは動作が遅い。元々高速化のための lazy import であるのに、これではあまり使い道がない。

2015年12月28日

lazy import 4

これまで、lazy import の手法を3つ紹介してきました。
本項では、通常のimport とこれら3つの手法について比較し、lazy import の使い所について考察します。

通常のimport
長所:
・最も標準のやり方
・面倒さは皆無
・importが冒頭にまとめられるので、依存関係を把握しやすい
短所:
・起動が遅くなる
lazy import 1 ローカルでのimport
長所:
・起動が早くなる
・仕組みが簡単
短所:
・毎回 import を書くのは面倒
・ソースコードの冒頭だけでは依存関係を把握できない
・import箇所を繰り返し通ると、速度低下に繋がる
lazy import 2 グローバル変数の利用
長所:
・起動が早くなる
・import箇所を繰り返し通っても、速度はほとんど落ちない
短所:
・毎回 import を書くのは面倒
・import部分を書くのに3行も必要。また、グローバル変数の変更を要する
・os.path のように . を含むモジュールをそのままでは import できない(os_path のように変形する必要がある)
lazy import 3 LazyImportクラス
長所:
・起動が早くなる
・import箇所で明示的に import 処理を書かなくてもよい
短所:
・専用のクラスが必要
・参照の度に getattr() が呼ばれるため、動作は遅い
・os.path のように . を含むモジュールをそのままでは import できない(os_path のように変形する必要がある)

こうして見ると、通常のimport が最も Pythonic な手法であることが分かります。
通常のimport のただひとつの短所を回避する lazy import の手法はいくつかあるけれど、その手法はどれも別の短所を持っている、といったところでしょうか。

import に際して取るべきルールを以下にまとめます。

  • 基本は、通常のimport を用いる。
  • 起動時間が重要ならば、import箇所を繰り返し通るかどうかによって、「ローカルでのimport」か「グローバル変数の利用」かを使い分ける。

2015年12月27日

lazy import 3

lazy import の可読性を向上させるテクニックを紹介します。


こちらで lazy import を紹介しました。実装例を再掲します。

def cat(filename):
    if filename.startswith('http://'):
        import urllib.request  # ここでimport
        print(urllib.request.urlopen(filename).read())
    else:
        print(open(filename).read())
lazy import によりプログラムの起動が速くなるのは利点ですが、一方で以下のような欠点もあります。
  • 冒頭に import が無いので、他モジュールへの依存関係を把握しづらい
  • 対象のモジュールを複数箇所で使用している場合、使用する度に import を書く必要がある

lazy import 専用のクラスを定義することで、上記の欠点を解消することができます。
以下にクラスの定義と使い方を記します。
# lazy import用クラス
class LazyImport(object):
    def __init__(self, name):
        self.__name = name
        self.__module = None

    def __getattr__(self, attr):
        # 初回のみimportする
        if self.__module is None:
            self.__module = __import__(self.__name)
        return getattr(self.__module, attr)

# 冒頭でimportと似た書式
urllib_request = LazyImport('urllib.request')

def cat(filename):
    if filename.startswith('http://'):
        # ここでimportを書かなくてよい
        print(urllib_request.urlopen(filename).read())
    else:
        print(open(filename).read())

LazyImportクラスを利用することで、先に挙げた2つの欠点がいずれも解消されました。
ただし、このクラスは参照の度に getattr() を呼ぶため、呼び出し速度は速くありません。

2015年12月26日

lazy import 2

lazy import の更なるテクニックを紹介します。


こちらで lazy import を紹介しました。lazy import の実装例を再掲します。

def cat(filename):
    if filename.startswith('http://'):
        import urllib.request  # ここでimport
        print(urllib.request.urlopen(filename).read())
    else:
        print(open(filename).read())
例として挙げた cat() 関数ですが、仮にこの関数が予想に反して URL 指定で何度も呼ばれたら、パフォーマンス的にはどうなるでしょう?
初回の import urllib.request では import の時間が丸々掛かります。これは避けようがありません。
2回目以降の import urllib.request では、初回ほども時間が掛かりません。が、初回ほども掛からないというだけで、import の処理自体は決して軽いものではありません。

この問題を解決するには、2回目以降ならば import を回避することが望まれます。
以下のようにグローバル変数と __import__() 関数を使うことで実現可能です。2回目以降は if文が実行されるだけなので、処理コストはずっと小さくなります。

urllib_request = None  # グローバル変数

def cat(filename):
    if filename.startswith('http://'):
        # 初回のみimportする
        if urllib_request is None:
            global urllib_request
            urllib_request = __import__('urllib.request')  # __import__()を使う

        print(urllib_request.urlopen(filename).read())
    else:
        print(open(filename).read())

2015年12月25日

lazy import 1

プログラムの起動を早くするためのテクニック lazy import を紹介します。


以下のようなソースコードがあったとします。

import urllib.request  # 冒頭でimport

def cat(filename):
    if filename.startswith('http://'):
        print(urllib.request.urlopen(filename).read())
    else:
        print(open(filename).read())
上で定義された cat() は、指定されたファイル名のファイルの内容を出力することを目的としています。追加の便利機能として、指定されたファイル名が URL である場合には、その URL のデータを出力します。
ここで、cat() の本来の目的はファイルの内容の出力であり、引数に URL が指定されることは滅多にない、とします。

この cat() が定義されたファイルを実行又は import すると、1行目で import urllib.request が実施されます。しかし、ここで import した urllib.request は結局は使われないことの方が多いということが分かっています。ということは、import に掛かった時間が丸々無駄になってしまう可能性が高いです。

この無駄な import を回避する手段を lazy import と言います(あえて日本語にすると「遅延import」と言った所でしょうか)。
やり方は簡単で、そのモジュールが必要となる直前に import を実施します。
cat() に lazy import を適用すると以下のようになります。

def cat(filename):
    if filename.startswith('http://'):
        import urllib.request  # ここでimport
        print(urllib.request.urlopen(filename).read())
    else:
        print(open(filename).read())


lazy import が効果的となるのは、プログラムの起動が頻繁に行われる場合です。
一度起動したらPCの電源が切れるまで動き続けるようなプログラムでは、それほど意味はありません。本当に起動が少し早くなるだけです。
逆に、短い周期で頻繁に起動されるプログラムであれば(操作の度に subprocess から呼ばれる、など)、その効果は大となります。