見出し画像

DP.06:生成させるオブジェクトの実体は1つだけ。-Singleton-【Python】

【1】Singletonパターン概要

Singletonパターンはオブジェクトを1つだけしか生成できないクラスを用意する書き方。
例えば、

・実行しているプログラムにおいて、グローバルな値(状態)を1つもたせて管理したい
・DBのコネクションプールを使い切らないようにDB接続関連のクラスインスタンスは1つにしたい
アクセスログ機能など、様々なアクセスがあってもそのプログラム内部で動いているログを管理するインスタンスは1つにしたい

などなど…

【2】例:HTTPリクエストを送信して、URLのリストを積んでいくURLFetcherクラス

例として、指定のURLにHTTPリクエストを送信して「ステータスコード:HTTP 200 OK」が返ってきたURLを、リストとして積んでいくようなクラスを作ってみる。

■URLFetcherクラス(Singleton未実施)

import urllib.parse
import urllib.request

class URLFetcher:

   def __init__(self) -> None:
       self.urls = []
   
   def fetch(self, url):
       req = urllib.request.Request(url)
       
       with urllib.request.urlopen(req) as response:
           if response.code == 200:
               the_page = response.read()
               #print(the_page) # ページの出力
               self.urls.append(url)

■使用イメージ(例外処理などは略)

my_fetcher = URLFetcher()

my_fetcher.fetch('https://www.python.org/')
# time.sleep(5)
my_fetcher.fetch('https://docs.python.org/3/')

print(my_fetcher.urls) 

#出力結果イメージ
['https://www.python.org/', 'https://docs.python.org/3/']

Singleton化していない場合、オブジェクトを複数生成する記述を行って実行すると、記述通りオブジェクトが複数生成される。

obj1 = URLFetcher()
obj2 = URLFetcher()

print(obj1)
print(obj2)
print(obj1 is obj2)

#実行結果例
<__main__.URLFetcher object at 0x0000027534BD4C70>
<__main__.URLFetcher object at 0x0000027534BD4BB0>
False

▲ obj1とobj2が別のものであることがわかる。

【補足】:HTTP通信をするモジュールについて

今回はpythonに標準で搭載している「urllib」を使用している。

「urllib2」は「python2用の古いもの」なので間違えないようにしよう。

もっとHTTP通信を簡単に書けるようにしている「Requests」や、「urllib3」を使うこともできる。

■Requests

■urllib3

【3】Singletonの実装の仕方

Singletonパターンは
『クラスインスタンスを生成する際に、すでにインスタンスが生成済みならその生成済みのインスタンスを返す』
という処理を実装すればよい。​

実現方法はいくつかある。今回は「typeクラスを継承したメタクラス」を使った方法で実装する。

■URLFetcherクラス(Singleton実施)

import urllib.parse
import urllib.request


# typeクラスを継承したメタクラスを用意する
class SingletonType(type):
   _instances = {} # オブジェクトの生成状況を保持する
   
   def __call__(cls, *args, **kwargs):

       # _instances内にオブジェクトが存在しない場合(未登録の場合)はオブジェクト生成処理をする
       if cls not in cls._instances:
           # super(※typeクラス)を使ってオブジェクトを生成させる
           cls._instances[cls] = super(SingletonType, cls).__call__(*args, **kwargs)

       return cls._instances[cls]


# SingletonTypeオブジェクト継承
# オブジェクト生成時に「__call__メソッド」を起動させて
# インスタンスの存在チェックと返すオブジェクトを制御している。
class URLFetcher(metaclass=SingletonType):

   def __init__(self):
       self.urls = []
   
   def fetch(self, url):
       req = urllib.request.Request(url)
       
       with urllib.request.urlopen(req) as response:
           if response.code == 200:
               the_page = response.read()
               #print(the_page)
               self.urls.append(url)

▲定番の書き方ではあるが、Pythonの仕様・挙動に熟練していないとよくわからない記述をしているように見えるかもしれない。
ようは、オブジェクト生成「URLFetcher( )」時に「__call__()」を起動させてインスタンスを返す処理を上書きしている、という感じ。

■動作確認

# 複数オブジェクトを生成する記述
my_fetcher1 = URLFetcher()
my_fetcher2 = URLFetcher()

print(my_fetcher1)
print(my_fetcher2)
print(my_fetcher1 is my_fetcher2)


my_fetcher1.fetch('https://www.python.org/') # 「my_fetcher1側でfetch()」
time.sleep(5)
my_fetcher2.fetch('https://docs.python.org/3/') # 「my_fetcher2側でfetch()」

# それぞれのオブジェクトで積んでいるURLリストを出力してみる
print(my_fetcher1.urls)
print(my_fetcher2.urls)

#出力結果
<__main__.URLFetcher object at 0x0000022F31316970>
<__main__.URLFetcher object at 0x0000022F31316970>
True
['https://www.python.org/', 'https://docs.python.org/3/']
['https://www.python.org/', 'https://docs.python.org/3/']

▲ オブジェクトが同じ=オブジェクトが1つしか生成されていないのがわかる。(積んでいるURLリストも共有されている)

【4】全体コード

import urllib.parse
import urllib.request
import time

class SingletonType(type):
   _instances = {}

   def __call__(cls, *args, **kwargs):
       if cls not in cls._instances:
           cls._instances[cls] = super(SingletonType, cls).__call__(*args, **kwargs)

       return cls._instances[cls]


class URLFetcher(metaclass=SingletonType):

   def __init__(self) -> None:
       self.urls = []

   def fetch(self, url):
       req = urllib.request.Request(url)
       
       with urllib.request.urlopen(req) as response:
           if response.code == 200:
               the_page = response.read()
               #print(the_page)
               self.urls.append(url)


def main():

   my_fetcher1 = URLFetcher()
   my_fetcher2 = URLFetcher()

   print(my_fetcher1)
   print(my_fetcher2)
   print(my_fetcher1 is my_fetcher2)

   my_fetcher1.fetch('https://www.python.org/')
   time.sleep(5)
   my_fetcher2.fetch('https://docs.python.org/3/')

   print(my_fetcher1.urls)
   print(my_fetcher2.urls)


if __name__ == '__main__':
   main()

【補足】:urllib.request が投げるExceptionについて

今回は例外処理を省略したが、通信に失敗する、ステータスコード404が返ってくるなどで例外が投げられる。詳細は以下参照。

■例:簡単に例外処理を組み込む場合(とりあえず「Exception」で拾う場合)

def main():

   my_fetcher = URLFetcher()

   my_urls = ['https://www.python.org/',
               'https://www.python.org/test', # 404が返ってくるページ
               'https://docs.python.org/3/',

               ]
   
   for url in my_urls:
       try:
           time.sleep(5) # 5秒ウェイト
           my_fetcher.fetch(url)
       except Exception as e:
           print(e)


   print(my_fetcher.urls)

# 実行結果例
HTTP Error 404: Not Found
['https://www.python.org/', 'https://docs.python.org/3/']

もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。