Pythonでシングルトンを実装するには?
クラスが使える言語の場合、クラス インスタンスが1つしか生成されないシングルトン(Singleton)が便利なケースがあります。ある解説書(『Expert Python Programming』:Tarek Ziade著)によれば、
Singleton restricts instantiation of a class to one object.
と書かれています。では、Pythonでどのように実装するのか? というのが今回のお話。
シングルトンは、クラスのインスタンス化を単一オブジェクトに規制する。
- 最低限のクラス定義
まずは、題材にする最低限の機能しか持たないクラスを定義してみましょう。
class Singleton(object): def __new__(clsObj, *args, **kwargs): instance = super(Singleton, object).__new__(clsObj, *args, **kwargs) return instance def __init__(self, *args, **kwargs): pass
- シングルトンの初歩
次に、このクラスを名実ともにシングルトン化してみましょう。
class Singleton(object): def __new__(clsObj, *args, **kwargs): if not hasattr(clsObj, "__instance__"): clsObj.__instance__ = super(Singleton, object).__new__(clsObj, *args, **kwargs) return clsObj.__instance__ def __init__(self, *args, **kwargs): pass
これで、最小限のシングルトン実装になりました。__new__()メソッドのhasattr(...)部分は、Python 2.5以前では
def __new__(clsObj, *args, **kwargs): if "__instance__" not in vars(): clsObj.__instance__ = super(Singleton, object).__new__(clsObj, *args, **kwargs) return clsObj.__instance__
という書き方もアリな方法でしたが、Python 3ではvars()やglobals()、locals()といったグローバル関数の使用が推奨されない(少なくともスコープにより推奨されない場合が出てくる)ため、今後は使わないのがバージョン依存性を減らすためにも、よいと思われます。
ただこの方法は、このSingletonクラスが継承されない前提でしか成り立たない実装です。というのも、基底クラスのコンストラクタが呼ばれる際に再帰処理が暗黙で実行されることが原因です。どういうことが起きるかというと、Singletonクラスのインスタンスであろうが、Singletonクラスのサブクラスのインスタンスであろうと、合わせて1つのインスタンスしか生成されない、という現象が起きます。
- 継承を考慮したシングルトン実装
そこで、基本的な仕組みは変えずに、もうちょっと賢い方法を考えましょう(多少冗長ではありますが、理解しやすさを優先しました)。
class sbase(object): def __new__(clsObj, *args, **kwargs): tmpInstance = None if not hasattr(clsObj, "_instanceDict"): clsObj._instanceDict = {} clsObj._instanceDict[hash(clsObj)] = super(sbase, clsObj).__new__(clsObj, *args, **kwargs) tmpInstance = clsObj._instanceDict[str(hash(clsObj))] elif not hasattr(clsObj._instanceDict, str(hash(clsObj))): clsObj._instanceDict[str(hash(clsObj))] = super(sbase, clsObj).__new__(clsObj, *args, **kwargs) tmpInstance = clsObj._instanceDict[str(hash(clsObj))] else: tmpInstance = clsObj._instanceDict[str(hash(clsObj))] return tmpInstance
先ほどまでの実装にあった__instance__アトリビュートはなくなり、_instanceDictというディクショナリ型のアトリビュートを使っています。hash()グローバル関数を使い、クラス オブジェクト(インスタンス生成前のクラス設計図)の一意なハッシュ コード(整数値)を取得し、辞書のキーにすることにより、クラスごとのインスタンス一意性を確保しています(getattr()グローバル関数を使うためにstr型にキャストしています)。
では、sbaseクラスによる実装をIDLEで軽く検証してみることにします。
>>> i1 = sbase() >>> i1 <__main__.sbase object at 0x01FB8330> >>> i2 = sbase() >>> i2 <__main__.sbase object at 0x01FB8330> >>> class s1(sbase):pass >>> i3 = s1() >>> i3 <__main__.s1 object at 0x01FB8E70> >>> i3._instanceDict {'33007512': <__main__.sbase object at 0x01FB8330>, '27442784': <__main__.s1 object at 0x01FB8E70>} >>> i4 = s1() >>> i1._instanceDict {'33007512': <__main__.sbase object at 0x01FB8330>, '27442784': <__main__.s1 object at 0x01FB8E70>}
i1とi2は基底クラスであるsbaseクラス、i3とi4はsbaseクラスを継承したs1クラスの、それぞれインスタンスです。IDLEの結果から、sbaseクラスのシングル インスタンスは
'33007512': <__main__.sbase object at 0x01FB8330>として、s1クラスのシングル インスタンスは
'27442784': <__main__.s1 object at 0x01FB8E70>として、それぞれ保持されていることが分かります。
デザインパターンは、GoFによる整理された思考ツール群です。BetaNewsは、思考ツールすべてを使う必要はなく、またそれらすべてを記憶している必要もないと考えています。とはいえ、使いこなすと便利であることは確かで、かつどの言語であっても(言語実装の制約はあったとしても)普遍的に利用できるものばかりです。Pythonでのデザインパターンを実装論まで含めてきちんと解説した書籍は非常に少ない(日本語では皆無に近い?)ので、引き続き、便利なデザパタにも焦点をあてていこうと思います。
![Expert Python Programming: Learn Best Practices to Designing, Coding, and Distributing Your Python Software Expert Python Programming: Learn Best Practices to Designing, Coding, and Distributing Your Python Software](https://images-fe.ssl-images-amazon.com/images/I/51mcvJ7kT-L._SL160_.jpg)
- 作者: Tarek Ziade
- 出版社/メーカー: Packt Publishing
- 発売日: 2008/09/30
- メディア: ペーパーバック
- クリック: 62回
- この商品を含むブログ (3件) を見る
Apacheのアクセス ログをPythonで解析するには?
Apacheログを効率よく解析するのは、SEO対策の面でも、パフォーマンス チューニングの面でも、かなり有効です。Apacheで一般的に使われるのはcommonとcombined形式のアクセスログで、かつcombinedio形式を独自にカスタマイズしたものなども使われます。とりあえず、一般的なcommon形式とcombined形式を正規表現化してみましょう。
- commonの場合、
^([0-9]{,3}\.[0-9]{,3}\.[0-9]{,3}\.[0-9]{,3}) ([^ ]{1,}) ([^ ]{1,}|\-) \[([0-9]{2}\/[A-Za-z]{3}\/[0-9]{1,4}:[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [+\-][0-9]{4})\] "([A-Z ]+) ([^"]*) ([^"]*)" ([0-9]{3}) ([0-9]{1,}|\-) "([^"]*|\-)" "([^"]+)"$ - combinedの場合、
^([0-9]{,3}\.[0-9]{,3}\.[0-9]{,3}\.[0-9]{,3}) ([^ ]{1,}) ([^ ]{1,}|\-) \[([0-9]{2}\/[A-Za-z]{3}\/[0-9]{1,4}:[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [+\-][0-9]{4})\] "([A-Z ]+) ([^"]*) ([^"]*)" ([0-9]{3}) ([0-9]{1,}|\-) "([^"]*|\-)" "([^"]+)"$
import re#スクリプト ファイルの冒頭部分に記述するのがよいclass FORMAT(object):
_UNSET_ = "unset"
_UNDEFINED_ = "undefined"
_UNKNOWN_ = "unknown"
_COMMON_ = "common"
_COMBINED_ = "combined"class logRow(object):
ipaddr = ""
clientID = ""
userID = ""
requestTimeStr = ""
requestTimeTzStr = ""
requestTimeTuple = False
method = ""
resource = ""
protocol = ""
statusCode = ""
responseBodySize = 0L
referer = ""
userAgent = ""
options = {}
def __init__(self, cols, **kwargs):
if isinstance(cols, list) or isinstance(cols, tuple):
if len(cols)>8:
self.ipaddr = cols[0]
self.clientID = cols[1]
self.userID = cols[2]
self.requestTimeStr = cols[3]
self.requestTimeTzStr = self.requestTimeStr[-5:]
self.requestTimeTuple = time.strptime(cols[3][:-6], "%d/%b/%Y:%H:%M:%S")
self.method = cols[4]
self.resource = cols[5]
self.protocol = cols[6]
self.statusCode = cols[7]
try:
self.responseBodySize = long(cols[8])
except ValueError:
self.responseBodySize = 0L
if len(cols)>10:
self.referer = cols[9]
self.userAgent = cols[10]
if len(cols)==12:
if isinstance(cols[11], dict):
self.options = cols[11]
else:
self.options = kwargs
def __dict__(self):
resDict = {}
resDict['ipaddr'] = self.ipaddr
resDict['clientID'] = self.clientID
resDict['userID'] = self.userID
resDict['requestTimeStr'] = self.requestTimeStr
resDict['requestTimeTzStr'] = self.requestTimeTzStr
resDict['requestTimeTuple'] = self.requestTimeTuple
resDict['method'] = self.method
resDict['resource'] = self.resource
resDict['protocol'] = self.protocol
resDict['statusCode'] = self.statusCode
resDict['responseBodySize'] =self.responseBodySize
resDict['referer'] = self.referer
resDict['userAgent'] = self.userAgent
resDict['options'] = self.options
return resDict
def __repr(self):
return repr(self.__dict__)class LogLine(object):
"""
Apache log parser class. Parsed field is set on field attribute.
Parsed field is the instance of structure class named as logRow.
"""
_FMT_COMMON = re.compile("""^([0-9]{,3}\.[0-9]{,3}\.[0-9]{,3}\.[0-9]{,3}) ([^ ]{1,}) ([^ ]{1,}|\-) \[([0-9]{2}\/[A-Za-z]{3}\/[0-9]{1,4}:[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [+\-][0-9]{4})\] "([A-Z ]+) ([^"]*) ([^"]*)" ([0-9]{3}) ([0-9]{1,}|\-) "([^"]*|\-)" "([^"]+)"$""")
_FMT_COMBINED = re.compile("""^([0-9]{,3}\.[0-9]{,3}\.[0-9]{,3}\.[0-9]{,3}) ([^ ]{1,}) ([^ ]{1,}|\-) \[([0-9]{2}\/[A-Za-z]{3}\/[0-9]{1,4}:[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [+\-][0-9]{4})\] "([A-Z ]+) ([^"]*) ([^"]*)" ([0-9]{3}) ([0-9]{1,}|\-) "([^"]*|\-)" "([^"]+)"$""")
_format = FORMAT._UNSET_
_rawString = ""
__FORMATS__ = {}
field = False
def __init__(self, line="", format=None):
"""
"""
self.__FORMATS__[FORMAT._COMMON_] = self._FMT_COMMON
self.__FORMATS__[FORMAT._COMBINED_] = self._FMT_COMBINED
if isinstance(line, basestring):
self._format = self.judgeFormat(line, format)
self._rawString = line
else:
self._format = FORMAT._UNSET_
if self._rawString!="" and self._format!=FORMAT._UNSET_ and self._format!=FORMAT._UNKNOWN_ and self._format!=FORMAT._UNKNOWN_:
self.parseLine()
@classmethod
def judgeFormat(clsObj, line="", format=""):
if format is None:
if clsObj._FMT_COMBINED.match(line):
format = FORMAT._COMBINED_
elif clsObj._FMT_COMMON.match(line):
format = FORMAT._COMMON_
else:
format = FORMAT._UNDEFINED_
if format=="common":
format = FORMAT._COMMON_
elif format=="combined":
format = FORMAT._COMBINED_
else:
format = FORMAT._UNDEFINED_
return format
def parseLine(self, rawLine=None):
if rawLine is None and self._format in self.__FORMATS__.keys() and self._rawString!="":
parsedTuple = self.__FORMATS__[self._format].findall(self._rawString)
if parsedTuple:
self.field = logRow(parsedTuple[0])
return True
elif rawLine is not None and isinstance(rawLine, basestring):
self._format = self.judgeFormat(rawLine)
result = self.parseLine()
return result
else:
return False
def getFormat(self):
return self._format
def setFormat(self, format):
if isinstance(format, basestring):
if format=="common":
self._format = FORMAT._COMMON_
elif format=="combined":
self._format = FORMAT._COMBINED_
def getRawString(self):
return self._rawString
def setRawString(self, line=None):
if isisntance(line, basestring):
self._rawString = line
LogLineクラスのコンストラクタにApacheのログ行(文字列)を食わせると、LogLineインスタンスのfieldアトリビュートに、解析されたログ行のキャプチャできた部分がLogRowクラス インスタンスとして保存されます。その際に、Apacheのアクセス ログ形式がcommonとcombined形式のどちらかでありさえすれば、行ごとに自動解析する仕組みにしました。
たとえば、
といったアクセス ログ行があった場合、fieldアトリビュートは
210.155.149.140 - - [06/Apr/2009:22:11:19 +0900] "POST /wordpress/wp-cron.php?check=83d0c18ebeedfebf737edad33a3142f1 HTTP/1.0" 200 - "-" "WordPress/2.7.1"
- ['requestTimeStr'] : 06/Apr/2009:22:11:19 +0900
- ['resource'] : /wordpress/wp-cron.php?check=83d0c18ebeedfebf737edad33a3142f1
- ['ipaddr'] : 210.155.149.140
- ['responseBodySize'] : 0
- ['userID'] : -
- ['clientID'] : -
- ['requestTimeTzStr'] : +0900
- ['options'] : {}
- ['referer'] : -
- ['userAgent'] : WordPress/2.7.1
- ['protocol'] : HTTP/1.0
- ['requestTimeTuple'] : (2009, 4, 6, 22, 11, 19, 0, 96, -1)
- ['method'] : POST
- ['statusCode'] : 200
Pythonコードをよく読まれた方は気付かれると思うのですが、LogLineクラスの_FMT_*アトリビュートに任意の正規表現オブジェクトを追加し、FORMATクラスの定数定義を併せて拡張することで、アクセス ログの解析方法をカスタマイズできます(継承してもいいし、継承しにくければ、インスタンス化してから直接属性追加してもいいかと)。