DP.01:オブジェクトの生成と利用を分離する。その1- FactoryMethod -【Python】
【1】Factory Method概要
簡単に言うと「Factory Method」は「特定の引数を指定すると、その引数によって適切なオブジェクトを返すような関数」を用意する書き方。
【2】例:JSON/XMLファイルのデータ読み込み
(1)サンプルデータファイル
サンプルファイルとして以下2種類のファイル(jsonとxml)を読み込んでデータを表示するプログラムを作成する。
■ movies.json
[
{"title":"Demon Slayer Mugen Train",
"year":2020,
"director":"Sotozaki Haruo", "cast":null, "genre":"Animation"},
{"title":"Evangelion: 3.0+1.0 Thrice Upon a Time",
"year":2021,
"director":"Anno Hideaki", "cast":null, "genre":"Animation"},
{"title":"Gintama THE Final",
"year":2021,
"director":"Miyawaki Chizuru", "cast":null, "genre":"Animation"},
{"title":"Dai Kome Soudou ",
"year":2021,
"director":"Motoki Katsuhide", "cast":"Inoue Mao", "genre":"Historical dramas"},
{"title":"Aladdin",
"year":2019,
"director":"Guy Ritchie", "cast":null, "genre":"Fantasy"},
{"title":"Fantastic Beasts:The Crimes of Grindelwald",
"year":2018,
"director":"David Yates", "cast":null, "genre":"Fantasy"},
{"title":"Bohemian Rhapsody",
"year":2018,
"director":null, "cast":"Rami Malek","genre":"Biopic"},
{"title":"Venom",
"year":2018,
"director":"Ruben Fleischer", "cast":"Edward Thomas Hardy", "genre":"SF"},
{"title":"One Cut of the Dead",
"year":2017,
"director":"ueda shinichirou", "cast":"Hamatsu Takayuki", "genre":"comedy horror"}
]
■person.xml
<persons>
<person>
<firstName>Tom</firstName>
<lastName>Smith</lastName>
<age>29</age>
<address>
<streetAddress>99 9th Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>40032</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">123 555-1234</phoneNumber>
<phoneNumber type="fax">987 555-6543</phoneNumber>
</phoneNumbers>
<gender>
<type>male</type>
</gender>
</person>
<person>
<firstName>Jim</firstName>
<lastName>Lia</lastName>
<age>19</age>
<address>
<streetAddress>100 87th Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>80076</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">234 555-6789</phoneNumber>
</phoneNumbers>
<gender>
<type>female</type>
</gender>
</person>
<person>
<firstName>Pattey</firstName>
<lastName>Lia</lastName>
<age>20</age>
<address>
<streetAddress>108 2nd Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>20021</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">456 555-789</phoneNumber>
<phoneNumber type="mobile">001 456-9876</phoneNumber>
</phoneNumbers>
<gender>
<type>female</type>
</gender>
</person>
</persons>
(2)補足:JSONファイルの読み込み方
jsonファイルはシンプルにjson.load()でロードしていく。
■例:jsonファイルのロード
import json
with open("movies.json") as f:
json_data = json.load(f) # jsonデータのロード
print(json_data)
例えばtitle部分だけを取り出したいときは以下のようにkey指定をすればよい
with open("movies.json") as f:
json_data = json.load(f)
for movie in json_data:
print(f"Title:{movie['title']}") # f-stringで出力
※f-stringについて
print()の文字列にfが付いているがこれは「f-string」というもの。特にjsonとは関係ないが、出力の記述が楽なので使用しているだけ。
(3)補足:xmlファイルの読み込み
今回はxmlファイルの処理に「xml.etree.ElementTree」モジュールを使う。
■例:xmlファイルの読み込み(パース)
import xml.etree.ElementTree as etree
xml_data = etree.parse("person.xml")
results = xml_data.findall(f".//person[lastName='Lia']") # lastNameタグに「Lia」と設定されているものすべてを検索
print(f'found:{len(results)} persons')
print("")
for result in results:
firstname = result.find('firstName').text
print(f'first name: {firstname}')
lastname = result.find('lastName').text
print(f'last name: {lastname}')
#同一タグを複数個持つパターンの出力。type属性とテキストの値を出力
[print(f"phone number({p.attrib['type']}):", p.text) for p in result.find('phoneNumbers')]
print("")
JSONとXMLではデータフォーマットが異なる。しかし、「特定の形式で書かれたファイルの中からデータを取り出したい」というところは共通している。
そこで、JSONファイルならJSONデータをロードするクラスオブジェクト、XMLファイルならXMLデータをロードするクラスオブジェクトを返すようなFactoryMethodを作ってみる。
【3】FactoryMethodを作る
作成するFactoryMethodは以下のような使い方をするイメージ。
※作成するFactoryMethodの使用イメージ
def main():
# extract_data_from()でJSONデータを取り扱うオブジェクトを作る
json_factory = extract_data_from('movies.json')
json_data = json_factory.parsed_data # json.load()した結果を返す
# 以下json_dataからkeyを指定して値を取得
... ...
# xmlファイルを渡すとxmlデータを取り扱うオブジェクトを返す
xml_factory = extract_data_from('person.xml')
xml_data = xml_factory.parsed_data # etree.parse()した結果を返す
# 以下xml_dataから必要なデータをフィルタして取得
... ...
▲指定する引数により返ってきているクラスオブジェクトは違うが「〇〇.parsed_data」という同じ名前のプロパティで、データ自体は取得できるようなつくりにする。
(1)FactoryMethodが返却するオブジェクトを用意
import json
import xml.etree.ElementTree as etree
###### JSON用
class JSONDataExtractor:
def __init__(self,filepath):
self.data = dict()
with open(filepath, mode='r', encoding='utf-8') as f:
self.data = json.load(f)
#property デコレータでメンバ変数のようにアクセス
@property
def parsed_data(self):
return self.data
###### XML用
class XMLDataExtractor:
def __init__(self,filepath):
self.tree = etree.parse(filepath)
@property
def parsed_data(self):
return self.tree
※「@propertyデコレータ」については以下参照。
(2)引数にあわせて適切なオブジェクトを返す関数(FactoryMethod)
def extract_data_from(filepath):
# 拡張子で代入するオブジェクトを切り替える
if filepath.endswith('json'):
extractor = JSONDataExtractor
elif filepath.endswith('xml'):
extractor = XMLDataExtractor
else:
raise ValueError(f'Cannot extract data from {filepath}')
return extractor(filepath)
### 使用イメージ ###
def main():
... ...
json_factory = extract_data_from('movies.json')
... ...
xml_factory = extract_data_from('person.xml')
▲今回は「endswith」を使って拡張子判定をして、対応するオブジェクトを返している。
【4】全体コード
import json
import xml.etree.ElementTree as etree
#### JSON ####
class JSONDataExtractor:
def __init__(self,filepath):
self.data = dict()
with open(filepath, mode='r', encoding='utf-8') as f:
self.data = json.load(f)
#property デコレータでメンバ変数のようにアクセスできる
@property
def parsed_data(self):
return self.data
#### XML ####
class XMLDataExtractor:
def __init__(self,filepath):
self.tree = etree.parse(filepath)
@property
def parsed_data(self):
return self.tree
#### factory Method ####
def extract_data_from(filepath):
# 拡張子で代入するオブジェクトを切り替える
if filepath.endswith('json'):
extractor = JSONDataExtractor
elif filepath.endswith('xml'):
extractor = XMLDataExtractor
else:
raise ValueError(f'Cannot extract data from {filepath}')
return extractor(filepath)
### 動作確認 ###
def main():
######## JSON #########
json_factory = extract_data_from('movies.json')
json_data = json_factory.parsed_data
#print(json_data)
# json_dataからkeyを指定して値を取得
for movie in json_data:
print(f"Title:{movie['title']}")
print("")
######## XML #########
xml_factory = extract_data_from('person.xml')
xml_data = xml_factory.parsed_data
# xml_dataから必要なデータをフィルタして取得
results = xml_data.findall(f".//person[lastName='Lia']")
for result in results:
firstname = result.find('firstName').text
print(f'first name: {firstname}')
lastname = result.find('lastName').text
print(f'last name: {lastname}')
#同一タグを複数個持つパターンの出力。type属性とテキストの値を出力
[print(f"phone number({p.attrib['type']}):", p.text) for p in result.find('phoneNumbers')]
print("-----")
if __name__ == '__main__':
main()
なお、わかりやすくするために「extract_data_from()」内で「raiseした例外」をそのままにしているが、本当は「try-except」をかませて例外に対する処理を入れたりする。
※「try-except」で例外処理をいれた場合
# extract_data_from()から分離
def dataextraction_factory(filepath):
if filepath.endswith('json'):
extractor = JSONDataExtractor
elif filepath.endswith('xml'):
extractor = XMLDataExtractor
else:
raise ValueError(f'Cannot extract data from {filepath}')
return extractor(filepath)
# try-exceptで例外処理
def extract_data_from(filepath):
factory_obj = None
try:
factory_obj = dataextraction_factory(filepath)
except ValueError as e:
print(e)
return factory_obj #例外処理を通過できたオブジェクトを返す
▲例外を投げる可能性がある処理を内部で分離しておくが、最終的にリターンするオブジェクトに変更はない。main()からコールするときのメソッドがextract_data_from()なのも変わらない。
もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。