[無料][2024年版] LINE Messaging API v3 + Python(Flask) でボットを作る [その7 - クラス化編]

## コードの冗長性

[前回](https://qiita.com/nozomiyamada/items/b80eaf5aef0c294c4acf)の記事で、モードごとの検索結果取得関数を定義しました。具体的には、`get_official_youtube` と `get_lyrics` の2つで、どちらも同じテーブルデータから、曲名をキーにして異なるコラムのデータを引っ張ってくるというものです。返信メッセージの整形方法などに多少の違いはあれど、コードの大半は共通しています。

~~~python:botfuncs.py
#####  MODE : GET OFFICIAL YOUTUBE  #####
def get_official_youtube(text:str) -> list:
	songtitles= search_song_title(text)
	if len(songtitles) == 0:  ## no candidate
		reply = 'NOT FOUND:\nPlease try again with different words.'
		messages = [TextMessage(text=reply)]
	elif len(songtitles) == 1:  ## only one candidate -> return URL
		reply = 'https://www.youtube.com/watch?v=' + DATA.loc[songtitles[0], 'official_youtube']
		messages = [TextMessage(text=reply)]
	else:
		quickreply = QuickReply(items=[])  ## instantiation 
		for song in songtitles:
			if len(song) > 20:  ## max characters : 20
				item = QuickReplyItem(action=MessageAction(label=song[:19]+'…', text=song))
			else:
				item = QuickReplyItem(action=MessageAction(label=song, text=song))
			quickreply.items.append(item)
		messages = [TextMessage(text='candidate songs:', quickReply=quickreply)]
	return messages

#####  MODE : GET LYRICS  #####
def get_lyrics(text:str) -> list:
	songtitles= search_song_title(text)
	if len(songtitles) == 0:  ## no candidate
		reply = 'NOT FOUND:\nPlease try again with different words.'
		messages = [TextMessage(text=reply)]
	elif len(songtitles) == 1:  ## only one candidate -> return lyrics
		reply = DATA.loc[songtitles[0], 'lyrics'].replace('<br>', '\n')  ## original data uses <br> instead of \n
		reply = f'{songtitles[0]} :\n\n' + reply  ## add song title as header
		messages = [TextMessage(text=reply)]
	else:
		quickreply = QuickReply(items=[])  ## instantiation 
		for song in songtitles:
			if len(song) > 20:  ## max characters : 20
				item = QuickReplyItem(action=MessageAction(label=song[:19]+'…', text=f'lyrics {song}'))  ## add prefix for next reply 
			else:
				item = QuickReplyItem(action=MessageAction(label=song, text=f'lyrics {song}'))
			quickreply.items.append(item)
		messages = [TextMessage(text='candidate songs:', quickReply=quickreply)]
	return messages
~~~

こういった冗長性を排除するために、関数は共通にしてキーワード変数などで指定して引っ張ってくるコラムや処理を変えるという方法もあります。ただし、それだと新しいモードを追加するたびに `if...elif...` を増やす必要もありますし、処理の修正の柔軟性には欠けます。そこで、今回は**クラス**を用いてみようと思います。「曲名から何かしらの情報を引っ張ってくる」ということで、`GetBySongTitle` とでも名付けます。

## クラスの設計

クラスを定義する際には「抽象化」こそが重要です。一見全然違うようなことをやっているように見えても、抽象的には同じ概念ならばそれは一つにまとめられます。

今回の場合、
- 検索文字列を受けとって、そこから曲名を探す
- ヒットしないならエラーメッセージを返す
- 1曲に特定できたなら、その曲の情報（YouTube動画 or 歌詞）を返す
- 2曲以上候補があるなら、クイックリプライにして選択させる

という処理は全く同じです。異なるのは、

1. データテーブルの参照するコラム
2. YouTube動画の場合、取得したIDの前にURLを付加する
3. 歌詞検索の場合、取得した歌詞の上に曲名をヘッダとして付ける
4. 歌詞検索の場合、返信用のメッセージアクションのテキストにモード選択 prefix である `lyrics` を付ける。YouTube動画なら何も付けない。

という点です。(2)と(3)に関しては、「取得データに何らかの文字列を付加して加工する」という意味では全く同じです。(4)も、「何も付けない」というのは「空文字 `''` を prefix として付ける」とみなすことができます。こう考えれば、

- 情報を取得するコラム名
- モード選択 prefix 名
- テキストの加工方法

を指定すれば、同一の処理ができるということになります。最後の「テキストの加工方法」に関しては、関数で渡してあげる必要があります。

百聞は一見に如かず、ということで完成したコードを載せます。

~~~python:botfuncs.py
class _GetBySongTitle:
	def __init__(self, col_name, mode_prefix='', decorate=lambda x: x):
		self.data = DATA[col_name] ## pd.Series : index - song title
		self.prefix = mode_prefix
		self.decorate = decorate  ## function to add sth to reply message

	def get(self, query:str) -> list:
		songtitles = search_song_title(query)
		if len(songtitles) == 0:  ## no candidate
			reply = 'NOT FOUND:\nPlease try again with different words.'
			messages = [TextMessage(text=reply)]
		elif len(songtitles) == 1:  ## only one candidate -> return URL
			reterieved = self.data[songtitles[0]]  ## retrieve data 
			reply = self.decorate(reterieved, songtitles[0])  
			messages = [TextMessage(text=reply)]
		else:
			quickreply = QuickReply(items=[])  ## instantiation 
			for song in songtitles:
				if len(song) > 20:  ## max characters : 20
					label = song[:19] + '…'
				else:
					label = song
				item = QuickReplyItem(action=MessageAction(label=label, text=self.prefix+song))
				quickreply.items.append(item)
			messages = [TextMessage(text='candidate songs:', quickReply=quickreply)]
		return messages

_GetYoutube = _GetBySongTitle(
	col_name='official_youtube',
	mode_prefix='',
	decorate=lambda youtubeID, songtitle: 'https://www.youtube.com/watch?v=' + youtubeID)  ## add YouTube URL
get_official_youtube = _GetYoutube.get

_GetLyrics = _GetBySongTitle(
	col_name='lyrics',
	mode_prefix='lyrics ',
	decorate=lambda lyrics, songtitle: f'{songtitle} :\n\n' + lyrics)  ## add song title as header
get_lyrics = _GetLyrics.get
~~~

クラス `_GetBySongTitle` のコンストラクタにおいて、prefix 名と加工方法の関数を受け取り、格納しています。また、コラム名が確定しているので、その列だけを `self.data` としてシリーズ型として格納します。

メソッド `get` においては、元々のコードとほぼ同じ処理をしています。修正点は、曲名が一つに確定してデータを取得した後に、テキスト加工を行う `self.decorate()` を呼んでいるという点と、クイックリプライにおいて付加する文字を `self.prefix` に統一していることくらいです。

クラス定義後のインスタンス化の部分で、元コードのモード特有の処理を指定します。`col_name` と `mode_prefix` は見たまんまです。`decorate` に関しては、無名関数 `lambda` を使うことでシンプルに記載しています。

最終的に、`_GetBySongTitle` クラスの `get` メソッドを元の関数名と同じになるように定義し直し、`app.py` にインポートさせます。これによって、`app.py` 側は一切修正することなく、同じ動作が保障されます。

## アンダースコア `_` の使用

ちなみになぜクラス名やインスタンス名にアンダースコア `_` を付けているかというと、`from botfuncs import *` でインポートした時に余計なもの（必要な関数以外）をエディタ内で表示させないようにする、という便宜的な理由です。別に付けなければいけないわけではありません。

これに関しては他に良記事がたくさんあるので、そちらも参考にしてください。

https://qiita.com/to-fmak/items/a41f03cc69c69a4e06ef

## 次回予告

モードの追加は今回作ったクラスを使えば簡単に行えるようになったので、次回はテーマを変えて **Flex Message** を使ったメッセージ送信を扱おうと思います。

[こちらの記事](https://qiita.com/nozomiyamada/items/4ac398159cdcf2f92056)も良ければ参考にして下さい。

## 目次 : [無料][2024年版] LINE Messaging API v3 + Python(Flask) でボットを作る

[GitHub レポジトリ](https://github.com/nozomiyamada/linebot/tree/main)
`older_version` 内に、各回時点での `app.py` と `requirements.txt` があります。

- [その0 - v3 での変更点](https://qiita.com/nozomiyamada/items/dff5f240e8c936d731fd)
- [その1 - 準備編](https://qiita.com/nozomiyamada/items/f6c6816e02359e08a74e)
- [その2 - テストアプリ編](https://qiita.com/nozomiyamada/items/87389aacb151b8418501)
- [その3 - 情報検索理論(前編)](https://qiita.com/nozomiyamada/items/d1bcedcc885c43794ff7)
- [その4 - 情報検索理論(後編)](https://qiita.com/nozomiyamada/items/6b0b30ebc36eeddc73d2)
- [その5 - tips 編](https://qiita.com/nozomiyamada/items/58918cd4024987ad1979)
- [その6 - モジュール化編](https://qiita.com/nozomiyamada/items/b80eaf5aef0c294c4acf)
- その7 - クラス化編
- [その8 - Flex Message 編](https://qiita.com/nozomiyamada/items/1be9ae292b01fb35273e)
- [その9 - ポストバック編](https://qiita.com/nozomiyamada/items/8a236a2d38f33827b4ce)
- [その10 - リッチメニュー & LIFF 編](https://qiita.com/nozomiyamada/items/ffbeeafbadfa6e98c711)