見出し画像

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': 'Demon Slayer Mugen Train', 'year': 2020, 'director': 'Sotozaki Haruo', 'cast': None, 'genre': 'Animation'}, {'title': 'Evangelion: 3.0+1.0 Thrice Upon a Time', 'year': 2021, 'director': 'Anno Hideaki', 'cast': None, 'genre': 'Animation'}, {'title': 'Gintama THE Final', 'year': 2021, 'director': 'Miyawaki Chizuru', 'cast': None, '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': None, 'genre': 'Fantasy'}, {'title': 'Fantastic Beasts:The Crimes of Grindelwald', 'year': 2018, 'director': 'David Yates', 'cast': None, 'genre': 'Fantasy'}, {'title': 'Bohemian Rhapsody', 'year': 2018, 'director': None, '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'}]

例えば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で出力
 

# 出力例
Title:Demon Slayer Mugen Train
Title:Evangelion: 3.0+1.0 Thrice Upon a Time
Title:Gintama THE Final
Title:Dai Kome Soudou
Title:Aladdin
Title:Fantastic Beasts:The Crimes of Grindelwald
Title:Bohemian Rhapsody
Title:Venom
Title:One Cut of the Dead

※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("")

# 出力例
found:2 persons
first name: Jim
last name: Lia
phone number(home): 234 555-6789

first name: Pattey
last name: Lia
phone number(home): 456 555-789
phone number(mobile): 001 456-9876

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()

#実行結果
Title:Demon Slayer Mugen Train
Title:Evangelion: 3.0+1.0 Thrice Upon a Time
Title:Gintama THE Final
Title:Dai Kome Soudou
Title:Aladdin
Title:Fantastic Beasts:The Crimes of Grindelwald
Title:Bohemian Rhapsody
Title:Venom
Title:One Cut of the Dead

first name: Jim
last name: Lia
phone number(home): 234 555-6789
-----
first name: Pattey
last name: Lia
phone number(home): 456 555-789
phone number(mobile): 001 456-9876
-----

なお、わかりやすくするために「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()なのも変わらない。

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