素人がプログラミングを勉強していたブログ

プログラミング、セキュリティ、英語、Webなどのブログ since 2008

連絡先: twitter: @javascripter にどうぞ。

キャッシュと効率

Life is beautiful: Python Hack : 噛めば噛むほどおいしくなるクロージャの話を見て、いろいろと気になるところがあったので書いてみる。

import yaml

def _get_from_disk():
     data = open('config.yaml').read().decode('utf8')
     config = yaml.load(data) # クロージャ内に隠蔽・保持されるローカル変数
     global get
     get = lambda : config   # 二回目からはconfigを返す無名関数を呼ぶ様に変更
     return get()

get = _get_from_disk   # 初回のみローダーを実行
http://satoshi.blogs.com/life/2009/11/python-hack.html

間違えました…。この部分はきちんと動いてますね。
まず、単純なコーディングミスのこと。
_get_from_diskを呼んだ時点でgetに関数がセットされるので、getへの代入は必要ない。
推測だが、_get_from_diskという関数は本当はgetという名前で、一度呼ぶと関数がキャッシュ済みのconfigを返すように差し替えられるという挙動をさせたかったのだと思う。

わかりづらいという事以外の欠点について触れられていなかったので、思いついただけ挙げる。
まず、普通の実装

import yaml

_config = None
def get():
    global _config
    if not _config:
        data = open('config.yaml').read().decode('utf8')
        _config = yaml.load(data)
    return _config
http://satoshi.blogs.com/life/2009/11/python-hack.html

では、
ここも間違いです…。
最初にgetが呼ばれたタイミングで設定ファイルがロードされるが、クロージャを使ったほうではコードが読み込まれた時点でconfigに値が束縛されてしまう。これはメモリ効率上よくない。
また、getを差し替える実装だとすると高階関数としてほかの関数に渡す事ができなくなる。この場合だと単に何度もloadされるだけだが、場合によってはバグの発生源となりむしろメンテナンス性は落ちる。

速度向上に役に立つかについても疑問に思う。

from time import time, strftime

_i = None
def a():
  global _i
  if not _i: i = 0
  return i

def b():
  i = 0
  global b
  b = lambda : i
  return i
v = 0

t = time()
for i in xrange(1000000):
  a()
  v += i
print time() - t

t = time()
for i in xrange(1000000):
  b()
  v += i

print time() - t

print v

このようなスクリプトを走らせたら、

% python a.py
0.412140846252
0.377043008804
999999000000

このような結果が得られた。100万回呼び出してたったの0.035msしか速度が変わらないオーバヘッドが、馬鹿にならないほどであることは少ないと思う。

さて、グローバル変数の使用を避け、かつ可読性もそれほど落ちない書き方を考えてみたい。
まず、関数のattributeを使う方法。

def fn():
  if not hasattr(fn, "__value"): fn.__value = "initialize"
  return fn.__value

外部からアクセスしようと思えばできてしまうのが玉に瑕。hasattrを使うのも少し見づらい。

classをつかう方法。

class Fn():
  def __init__(self):
    self.__value = "initialize"

  def __call__(self):
    return self.__value

fn = Fn()

Fnの初期化時にファイル名をオプションとして渡せるようにしたりできるので、拡張性の面では優れている。
その他の方法として、オプション引数の初期化が定義時最初の呼び出し時にしか行われないことを利用した、

def f(value="initialize"):
  return value

などもある。ただしこれは将来のバージョンで使えるかどうかわからないハックである。

この話で必要とされているのはクロージャではなくstatic変数であり、様々な代替手段があるので可読性と速度を天秤にかけ適切な手段を選ぶべきだ。