コードの冗長性
前回の記事で、モードごとの検索結果取得関数を定義しました。具体的には、get_official_youtube
と get_lyrics
の2つで、どちらも同じテーブルデータから、曲名をキーにして異なるコラムのデータを引っ張ってくるというものです。返信メッセージの整形方法などに多少の違いはあれど、コードの大半は共通しています。
##### 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曲以上候補があるなら、クイックリプライにして選択させる
という処理は全く同じです。異なるのは、
- データテーブルの参照するコラム
- YouTube動画の場合、取得したIDの前にURLを付加する
- 歌詞検索の場合、取得した歌詞の上に曲名をヘッダとして付ける
- 歌詞検索の場合、返信用のメッセージアクションのテキストにモード選択 prefix である
lyrics
を付ける。YouTube動画なら何も付けない。
という点です。(2)と(3)に関しては、「取得データに何らかの文字列を付加して加工する」という意味では全く同じです。(4)も、「何も付けない」というのは「空文字 ''
を prefix として付ける」とみなすことができます。こう考えれば、
- 情報を取得するコラム名
- モード選択 prefix 名
- テキストの加工方法
を指定すれば、同一の処理ができるということになります。最後の「テキストの加工方法」に関しては、関数で渡してあげる必要があります。
百聞は一見に如かず、ということで完成したコードを載せます。
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 *
でインポートした時に余計なもの(必要な関数以外)をエディタ内で表示させないようにする、という便宜的な理由です。別に付けなければいけないわけではありません。
これに関しては他に良記事がたくさんあるので、そちらも参考にしてください。
次回予告
モードの追加は今回作ったクラスを使えば簡単に行えるようになったので、次回はテーマを変えて Flex Message を使ったメッセージ送信を扱おうと思います。
こちらの記事も良ければ参考にして下さい。
目次 : [無料][2024年版] LINE Messaging API v3 + Python(Flask) でボットを作る
GitHub レポジトリ
older_version
内に、各回時点での app.py
と requirements.txt
があります。