関数=クラスのメソッドなモジュール



Life is beautiful: Python Hack : 噛めば噛むほどおいしくなるクロージャの話

の解法に疑問があったので、自分なりに書き直してみます。




書き直すのはこのconfigモジュール。

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



Life is beautiful: Python Hack : 噛めば噛むほどおいしくなるクロージャの話

では、クロージャを使うと良いとして、次のコードを提示しています。

#config
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   # 初回のみローダーを実行



このコードの問題は、from config import get や 高階関数
getを別の変数に代入した時に起きます。



設定ファイルを読み込む例だと説明しづらいので、
乱数をキャッシュするモジュールを考えてみます。

#random_cache
import random

#初回の乱数をキャッシュして、2回目以降は同じ値を返す

def _get_from_random():
    cache = random.random()
    global get
    get = lambda : cache
    return get()

get = _get_from_random

$>python
Python 2.6.4 (r264:75708, Oct 26 2009, 08:23:19) [MSC v.1500 32 bit (Intel)] on
win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from random_cache import get as rc_get
>>> rc_get()
0.37072846241539548
>>> rc_get()
0.12122267986050828 #あれー!?


これは、get = lambda : cache という代入が、
"random_spam 内の get変数の中身" に変更を与える操作だからです。


別の変数であるrc_getには関係ないのです。


では、どう書き直せばいいかと言うと、クラスを使えばよいと思います。

#config2.py

class Config(object):
    def _get_config(self):
        data = open('config.yaml').read().decode('utf8')
        return yaml.load(data)
    
    def cached_get(self):
        return self.config
    
    def get(self):
        self.config = self._get_config()
        self.get = self.cached_get
        return self.config
    
_Config = Config()
def get():
    return _Config.get()



config2は、標準ライブラリのturtleの様に、
モジュールの関数を直接呼び出す事も、インスタンス化してメソッドを呼ぶ事ができます。