さて、ここまで作成してきたロガークラスを利用する際、全ての箇所で常に異なるログファイルに書き込みたい、ということはないと思います。ですから、あるログファイルに書き込むためのロガーオブジェクトが既にインスタンス化済みであればそれを使用し、なければ新規作成する、といった動きが理想なわけです。
オブジェクト指向プログラミングの利点を失うことなくこれを実現するためには、オブジェクト作成の主導権を、ロガーの利用者ではなく我々が握っておく必要があります。
オブジェクト作成プロセスをコントロールするための実装例を見てみましょう:
class SingletonExample:
class __SingletonExample:
def __init__(self):
self.val = None
def __str__(self):
return f'{repr(self)} {self.val}'
instance = None
def __new__(cls):
if not SingletonExample.instance:
SingletonExample.instance = SingletonExample.__SingletonExample()
return SingletonExample.instance
どうしてこの実装で、このクラスのオブジェクトは常に1つしか作成されないことが保証されるのでしょうか?既にオブジェクトが作成済みの場合はそのオブジェクトが提供されることが保証されるのでしょうか?少しづつ見ていきましょう。
本来のロガークラスはインナークラスである _SingletonExample になります。しかしこのクラスはアウタークラスである SingletonExample に覆い隠されて外部からは見えません。このように実装することで、ロガークラスのオブジェクトの作成、属性へのアクセスを全て我々のコントロール下に置くことが可能になります。
さて、実装した val 変数を利用して、SingletonExample クラスのオブジェクトは本当に1つしか作成されないのかを検証してみましょう:
from singleton import SingletonExample
obj1 = SingletonExample()
obj1.val = '最初に作成したオブジェクトです'
print(f'Object 1: {obj1}')
print('----------------------------')
obj2 = SingletonExample()
obj2.val = '2番目に作成したオブジェクトです'
print(f'Object 1: {obj1}')
print(f'Object 2: {obj2}')
Object 1: <singleton.SingletonExample.__SingletonExample object at 0x0000017672507C40> 最初に作成したオブジェクトです
----------------------------
Object 1: <singleton.SingletonExample.__SingletonExample object at 0x0000017672507C40> 2番目に作成したオブジェクトです
Object 2: <singleton.SingletonExample.__SingletonExample object at 0x0000017672507C40> 2番目に作成したオブジェクトです
2番目に作成したオブジェクトの属性に対する書き込みは最初に作成したオブジェクトにも反映されています。また、2つのオブジェクトが存在するメモリアドレスも同じであることが分かります。すなわち、SingletonExample クラスのオブジェクトは1つしか作成されていないということになります。
このように、あるクラスのオブジェクトが1つしか作成されないことを保証する実装パターンのことを「シングルトンパターン (singleton pattern)」と言っています。
Singleton pattern に対する主要な批判の1つは、コーディングにおいて避けるべきこと、とされている「グローバル属性の使用」です。グローバル変数の使用を避けるべき理由の1つは、プログラム内のある場所でこの値を変更してしまった場合、全く関連性のない他の場所で予期しない結果を招く可能性が生じ、最も追跡し辛いバグの原因となりかねないからです。
ですから、シングルトンパターン (or グローバル変数) を利用するのは、「ログ取得 (logging)」のような他の機能に影響を及ぼす可能性がない場合に限るべきです。他の用途としては、キャッシング (caching)、ロードバランシング (load balancing)、ルートマッピング (route mapping) などが考えられますが、これらにおけるデータの流れは全て一方向に限られており、また、シングルトンオブジェクト自身も変更の必要がない (immutable である)、という特徴があります。シングルトンオブジェクトが immutable である以上、そのグローバル属性を通してプログラムの他のパートに悪影響を及ぼす心配はありません。
さて、この Singleton pattern 例を参考にして、1つのインスタンスしか作成できないロガークラスを作成してみてください。この例でいえば、__SingletonExample インナークラスが実際のロガークラスになるはずです。機能面での変更が必要なければ、
こちらの記事 で作成した Logger クラスの内容をそのまま利用するだけです。
Singleton バージョン Logger 実装例 (singleton_logger.py):
from pathlib import Path
class SingletonLogger:
class __SingletonLogger:
def __init__(self, file_name):
self.file_name = file_name
def _write_log(self, level, msg):
with Path(self.file_name).open('a', encoding='utf-8') as log_file:
log_file.write(f'[{level}] {msg}\n')
def critical(self, msg):
self._write_log('CRITICAL', msg)
def error(self, msg):
self._write_log('ERROR', msg)
def warn(self, msg):
self._write_log('WARN', msg)
def info(self, msg):
self._write_log('INFO', msg)
def debug(self, msg):
self._write_log('DEBUG', msg)
def __str__(self):
return f'{repr(self)} {self.file_name}'
instance = None
def __new__(cls, file_name):
if not SingletonLogger.instance:
SingletonLogger.instance = SingletonLogger.__SingletonLogger(file_name)
return SingletonLogger.instance
実行例 (test_singleton_logger.py):
from singleton_logger import SingletonLogger
obj1 = SingletonLogger('test.log')
print(f'Object 1: {obj1}')
print('----------------------------')
obj2 = SingletonLogger('another.log')
print(f'Object 1: {obj1}')
print(f'Object 2: {obj2}')
print(dir(obj1))
obj1.info('ログメッセージ info 用')
Object 1: <singleton_logger.SingletonLogger.__SingletonLogger object at 0x000001BB6C8F7C40> test.log
----------------------------
Object 1: <singleton_logger.SingletonLogger.__SingletonLogger object at 0x000001BB6C8F7C40> test.log
Object 2: <singleton_logger.SingletonLogger.__SingletonLogger object at 0x000001BB6C8F7C40> test.log
['__class__', ..., '_write_log', 'critical', 'debug', 'error', 'file_name', 'info', 'warn']
[INFO] ログメッセージ info 用
SingletonLogger クラスは singleton として実装しましたから、当たり前ですがオブジェクトは1つしか作れません。では、異なるログファイルに書き込むためのオブジェクトを作成したい場合はどうしたらよいのでしょうか?
1つの方法は、クラス変数 instance を、ログファイル名を key、対応するオブジェクトを value とする dict として実装することです。そして、オブジェクト作成時に渡されたファイル名が instance dict の key として存在すればその value を返し、なければ新規にオブジェクトを作成します:
class SingletonLogger:
class __SingletonLogger:
...
instance = {}
def __new__(cls, file_name):
if file_name not in SingletonLogger.instance:
SingletonLogger.instance[file_name] = SingletonLogger.__SingletonLogger(file_name)
return SingletonLogger.instance[file_name]