検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
当サイトのドメイン名は " getwebtips.net " です。
トップレベルドメインは .net であり、他の .com / .shop といったトップレベルドメインのサイトとは一切関係ありません。
practical_python_design_patterns

Practical Python Design Patterns - Python で学ぶデザインパターン: Singleton Patterns Part. 3 「いよいよシングルトンパターン」の巻 投稿一覧へ戻る

Published 2022年5月17日14:26 by T.Tsuyoshi

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

Practical Python Design Patterns - The Singleton Pattern 編
「いよいよシングルトンパターン」の巻

Cleaning It Up
(いよいよ Singleton Pattern)

さて、ここまで作成してきたロガークラスを利用する際、全ての箇所で常に異なるログファイルに書き込みたい、ということはないと思います。ですから、あるログファイルに書き込むためのロガーオブジェクトが既にインスタンス化済みであればそれを使用し、なければ新規作成する、といった動きが理想なわけです。
オブジェクト指向プログラミングの利点を失うことなくこれを実現するためには、オブジェクト作成の主導権を、ロガーの利用者ではなく我々が握っておく必要があります。
オブジェクト作成プロセスをコントロールするための実装例を見てみましょう:
singleton.py
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つしか作成されないことが保証されるのでしょうか?既にオブジェクトが作成済みの場合はそのオブジェクトが提供されることが保証されるのでしょうか?少しづつ見ていきましょう。
まず目につくのは、class の中で別の class が定義されている、ということでしょう。このクラス名 (__SingletonExample) の先頭にはアンダースコアがついています。これは既に説明したように、「このクラスは private だから、外部から勝手に呼び出してインスタンス化しないで」ということを他のプログラマに伝えるためのものです。
我々のロガー機能はこのプライベートクラスに実装することになりますが、現時点では singleton の説明に集中するために省略しています。
また、後の動作確認で利用するために、このプライベートクラスには属性を1つ (val) とメソッドを2つ (__init__, __str__) 定義してあります。
これは「クラス属性 (変数)」です。すなわち、この SingletonExample クラスを雛形として作成された全てのオブジェクトに「共通する」属性です。つまり、「もし」このクラスのオブジェクトが複数存在し、ある1つのオブジェクトでこの値を変更すれば、他のオブジェクトからこの変数を参照した場合の値もその変更された値になります。
この __new__() では、"cls" という名前のパラメータを受け取っていますが、一方で "self" パラメータは受け取っていません。これは、この __new__() メソッドが「クラスメソッド」だからです。このメソッドは、クラスがインスタンス化される際に Python から呼び出される「特殊関数」で、オブジェクトを作成するための「雛形」をパラメータとして受け取り、作成したオブジェクトを返します。クラスメソッドが取る第1パラメータとして "cls" という名前を利用するのも "self" 同様 Python における慣習です。他のプログラマを戸惑わせたいのであれば、あなたは好きな名前を使用することができますが、お薦めはしませんョ。
先程も少し触れましたが、__new__() のように、最初と最後を2つのアンダースコアで挟まれたメソッドは magic methods (特殊関数 / ダンダーメソッド) と呼ばれるもので、Python インタプリタによって呼び出されるメソッドです。
__new__() メソッドが呼び出されると、まず instance クラス変数の値をチェックし、もし値が None (今までに作成されたオブジェクトはない) であれば private クラス __SingletonExample クラスのオブジェクトを新規作成し instance 変数に保存します。一方、値が None でなければすでにオブジェクトが作成され保存されている、ということです。どちらにしても最終的に instance 変数の値を返しますが、ロガーの利用者からすれば、返されてきたロガーオブジェクトが新規作成されたものかすでに作成済みのものかは判断できません。オブジェクト作成の主導権は我々が握っているのです。
本来のロガークラスはインナークラスである _SingletonExample になります。しかしこのクラスはアウタークラスである SingletonExample に覆い隠されて外部からは見えません。このように実装することで、ロガークラスのオブジェクトの作成、属性へのアクセスを全て我々のコントロール下に置くことが可能になります。
さて、実装した val 変数を利用して、SingletonExample クラスのオブジェクトは本当に1つしか作成されないのかを検証してみましょう:
test_sigleton.py
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> 番目に作成したオブジェクトです
Object 2: <singleton.SingletonExample.__SingletonExample object at 0x0000017672507C40> 番目に作成したオブジェクトです
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']
ファイル test.log の内容:
[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]

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)