Python Udemy講座 MVCモデル robot.py

Python学習【365日チャレンジ!】113日目のマスターU(@Udemy11)です。

昨日もついついAmazonプライムで明け方までシリーズ物を観てしまい、寝不足絶好調です。

前にテレビでちらっと見てからちょっとだけ気になっていたアニメ【Dr.STONE】のシリーズがAmazonプライムにあったので、ちょっとだけ観ようとおもったのが最後、すべてのエピソードをコンプリートしてしまいました。

このままでは単行本にも手を出してしまいそうです。

ほんとやばいですAmazonプライム

Amazonプライム Dr.STONE

それでは、今日もPython学習にはいりましょう!

昨日の復習

昨日は、modelフォルダに用意するCSVファイルの操作をするためのranking.pyについて学習しました。

CSVからデータを取り出したり並べ替えたリしたデータを活用したり、書き込んだりするパートを担当していましたが、ファイルのパスを取得するコードは、2日前に学習したconsole.pyと同じような処理ができました。

ranking.pyは、CSVの書き込み読み込みを担当するので、少しコードが難しくなりましたが、非常に重要なファイルなので、しっかりと昨日の記事で復習しておきましょう!

それでは、いよいよJarvisの本体ともいえるrobot.pyのコードをみていきましょう!

robot.py

Jarvisの対話をコントロールするrobot.pyは用意した会話を参照して、ユーザーの情報をinput()で取得するコードがメインとなっています。

from jarvis.models import ranking
from jarvis.views import console


DEFAULT_ROBOT_NAME = 'Jarvis'


class Robot(object):

    def __init__(self, name=DEFAULT_ROBOT_NAME, user_name=''):
        self.name = name
        self.user_name = user_name

    def hello(self):
        while True:
            template = console.get_template('hello.txt')
            user_name = input(template.substitute({'robot_name': self.name}))

            if user_name:
                self.user_name = user_name.title()
                break


class SportsRobot(Robot):

    def __init__(self, name=DEFAULT_ROBOT_NAME):
        super().__init__(name=name)
        self.ranking_model = ranking.RankingModel()

    def _hello_decorator(func):
        def wrapper(self):
            if not self.user_name:
                self.hello()
            return func(self)
        return wrapper

    @_hello_decorator
    def recommend_sport(self):
        new_recommend_sport = self.ranking_model.get_most_popular()
        if not new_recommend_sport:
            return None

        finish_recommend_sports = [new_recommend_sport]
        while True:
            template = console.get_template('recommend.txt')
            is_yes = input(template.substitute({
                'robot_name': self.name,
                'user_name': self.user_name,
                'sport': new_recommend_sport
            }))

            if is_yes.upper() == 'Y' or is_yes.upper() == 'YES':
                break

            if is_yes.upper() == 'N' or is_yes.upper() == 'NO':
                new_recommend_sport = self.ranking_model.get_most_popular(not_list=finish_recommend_sports)
                if not new_recommend_sport:
                    break
                finish_recommend_sports.append(new_recommend_sport)

    @_hello_decorator
    def ask_user_favorite(self):
        while True:
            template = console.get_template('like.txt')
            sport = input(template.substitute({
                'robot_name': self.name,
                'user_name': self.user_name,
            }))
            if sport:
                self.ranking_model.increment(sport)
                break

    @_hello_decorator
    def thank_you(self):
        template = console.get_template('goodby.txt')
        print(template.substitute({
            'robot_name': self.name,
            'user_name': self.user_name,
        }))

robot.pyがインポートするモジュールは、昨日学習したranking.pyviewフォルダ内のconsole.pyになります。

ranking.pyは、同じ階層にあるので、import rankingだけでも問題はないのですが、パッケージの場合は、メインの実行ファイルであるmain.pyからのパスを指定することが推奨されているので、jarvisフォルダからのパスで指定しています。

モジュールをインポートしたあとは、5行目のDEFAULT_ROBOT_NAME = 'Jarvis'で、対話ロボットの名前を、グローバル変数に代入しています。

次にそれぞれのクラスと関数を個別にチェックしていきます。

class Robot(object)

最初に定義するのは、対話ロボットのベースクラスです。

class Robot(object):

    def __init__(self, name=DEFAULT_ROBOT_NAME, user_name=''):
        self.name = name
        self.user_name = user_name

ベースクラスでは、初期化処理として、グローバル変数DEFAULT_ROBOT_NAMEに代入したJarvisself.nameに代入し、後ほど取得する引数user_nameを空にしたものをself.user_nameに代入しています。

def hello(self)

ベースクラスには、初期化以外に、挨拶をするhello(self)関数を定義しています。

    def hello(self):
        while True:
            template = console.get_template('hello.txt')
            user_name = input(template.substitute({'robot_name': self.name}))

            if user_name:
                self.user_name = user_name.title()
                break

この関数では、whileステートメントでuser_nameを取得するまで繰り返すループが定義されています。

変数templateにインポートしたconsoleモジュールのget_template()に引数hello.txtにした返り値を代入します。

こちらの記事を見ればわかるように、変数templateには、hello.txtを次のように整形した値が代入されます。

////////////////////////////////////////////////////////////
hello.txtの中身
////////////////////////////////////////////////////////////

17行目では、テンプレートの中の文章で定義されている変数robot_nameself.nameを代入した文章を表示し、ユーザーからインプットされたデータをuser_nameに代入します。

ユーザーのインプットが完了すると、19行目でTrueが返されるので、user_nameの頭文字を大文字にして、class=”art”>self.user_nameに代入して関数を抜けます。

class SportsRobot(object)

次にRobotクラスを継承した対話ロボットのクラス、つまりJarvisを作成していきます。

class SportsRobot(Robot):

    def __init__(self, name=DEFAULT_ROBOT_NAME):
        super().__init__(name=name)
        self.ranking_model = ranking.RankingModel()

スポーツについて尋ねるロボットなので、クラス名はSportsRobotにしています。

酒井さんの講座では、お気に入りのレストランを尋ねるサンプルだったのですが、お気に入りのスポーツを尋ねるロボットに変更しました。

初期化では、引数nameにグローバル変数のDEFAULT_ROBOT_NAMEを代入し、親クラスの__init__を呼び出して、引数nameに、このクラスの引数nameを代入しています。

初期化の最後では、インポートしたrankingモジュールのRankingModelクラスからインスタンス(オブジェクト)を作成して、self.ranking_modelとしています。

rankingモジュールについては、上記の記事で詳しく解説していますが、RankingModelインスタンスは、CSVを書き込んだり読み込んだりするためのクラスオブジェクトになります。

@_hello_decorator

こちらは、デコレーターになり、次の関数以降で使うために定義しています。

    def _hello_decorator(func):
        def wrapper(self):
            if not self.user_name:
                self.hello()
            return func(self)
        return wrapper

デコレーターについては、次の記事で学習しました。

関数の前後でなにか処理をさせたいときにデコレーターを作成して、@をつけて、デコレートしたい関数の一つ前の行に記述しました。

今回のデコレーターなら、@_hello_dekoratorですね。

次の関数以降に@_hello_dekoratorをつけていますが、これは後で学習するControllerの実行ファイルで、hello()関数が実行されずにself.user_nameが取得できていない場合、テンプレートにユーザーの名前が表示できないからです。

それぞれの文章を出力するテンプレートには、変数self.user_nameが使われるため、デフォルト引数で空を代入しているので、文章がおかしくなってしまいます。

基本的に、Controllerの実行ファイルで、hello()関数が実行されるなら、self.user_nameを取得できるので、問題はありません。

念のため、デコレーターを使って、実行する関数より先にhello()を実行することで、self.user_nameを取得できるようにしているということです。

なので、個人的には、このデコレーターは不要なんじゃないかと感じていますが、実践では、ここまでカバーできるようなコードを書いたほうがいいのかもしれません。

デコレーター@_hello_decolatorの解説をしたので、次以降の関数についてはデコレータが付いていますが、説明は省きます。

def recommend_sport(self)

おすすめのスポーツが好きかどうか尋ねるための関数です。

    def recommend_sport(self):
        new_recommend_sport = self.ranking_model.get_most_popular()
        if not new_recommend_sport:
            return None

        finish_recommend_sports = [new_recommend_sport]
        while True:
            template = console.get_template('recommend.txt')
            is_yes = input(template.substitute({
                'robot_name': self.name,
                'user_name': self.user_name,
                'sport': new_recommend_sport
            }))

            if is_yes.upper() == 'Y' or is_yes.upper() == 'YES':
                break

            if is_yes.upper() == 'N' or is_yes.upper() == 'NO':
                new_recommend_sport = self.ranking_model.get_most_popular(not_list=finish_recommend_sports)
                if not new_recommend_sport:
                    break
                finish_recommend_sports.append(new_recommend_sport)

39行目でRankingModelのインスタンスself.ranking_modelget_most_popular()を実行して、最もライキングの高いスポーツ名を変数new_recommend_sportに、代入しています。

40行目、41行目で、new_recommend_sportがない場合、Noneを返しています。つまり、この関数では何も実行されないということです。

new_recommend_sportが存在する場合は、その値をリスト変数finish_recommend_sportsに代入します。

44行目で、whileループに入りますが、45行目でconsoleモジュールのget_template関数で、recommend.txtをフォーマット(整形)してから、46行目から50行目でテンプレートの整形した文章にロボット名やユーザー名、スポーツ名を変数に代入し出力、input()で取得したYesかNoの値を変数is_yesに代入しています。

52行目、53行目で、変数is_yesをすべて大文字にして、YYESの場合はループを抜けます。

is_yesの大文字化した値がNNOの場合は、インスタンスself.ranking_modelget_most_popular()関数の引数not_listにリスト変数finish_recommend_sportを代入して取得した値でnew_recommend_sportを上書きします。

57行目から59行目では、new_recommend_sportがない場合はループを抜けて、値が存在した場合はその値をリスト変数finish_recommend_spotrsに加えます。

最終的に、おすすめのスポーツが好きかどうかを尋ねるパートは次の条件分岐のようになります。

  1. データがない場合:何もしない
  2. データがある場合:ランキングの高いものが好きかどうか聞く
  3. 回答がYesの場合:この質問は終了
  4. 回答がNoの場合:データがなければ終了、あれば次にランキングが高いスポーツが好きかどうか聞く

回答がYesの場合はすぐにループを抜けますが、Noの場合は、データが無くなるまでおすすめのスポーツを変更して質問を繰り返します。

def ask_user_favorite(self)

次の関数では、大好きなスポーツを尋ねます。

    def ask_user_favorite(self):
        while True:
            template = console.get_template('like.txt')
            sport = input(template.substitute({
                'robot_name': self.name,
                'user_name': self.user_name,
            }))
            if sport:
                self.ranking_model.increment(sport)
                break

consoleモジュールのget_templateを使ってlike.txtをフォーマットしてtemplateに代入し、整形したテンプレートの文章に、ロボットの名前やユーザーの名前を代入して出力、input()で値を取得するのは他の関数と同じですね。

69行目で、取得したsportがあれば、self.ranking_modelインスタンスのincrement(sport)を実行して、CSVに値を追加してループを抜けます。

increment関数の処理についてはこちらの記事を御覧ください。

def thank_you(self)

いよいよ最後の関数ですが、終わりの挨拶で締めくくります。

    def thank_you(self):
        template = console.get_template('goodby.txt')
        print(template.substitute({
            'robot_name': self.name,
            'user_name': self.user_name,
        }))

他の関数同様に、consoleモジュールのget_templateを使い、goodby.txtを整形し、ロボットの名前とユーザー名を文章の中の変数に代入して出力しています。

テンプレートにつなげる

あとはテンプレートの文章を仕上げれば、Jarvisほぼ完成します。

標準ライブラリ以外の他のモジュールをインポートしていないPythonファイルから取り掛かりましたが、コードを書いている途中で他のファイルとのつながりを考える必要が出てくるため、一つ一つのファイルを仕上げてから次のファイルに進むのではなく、他のファイルとのつながりが出てきた時点で別のファイルのコードも書き出したほうがいいかもしれません。

同時進行で別ファイルのコードを書いていくのか、一つ一つのファイルを仕上げてから次のファイルに取り掛かるのがいいかは人それぞれなので、いくつかコードを書きながら、自分にあった方法を見つけていくしかありません。

個人的には、ガッツリ全体設計を仕上げて個別のコードを書くのではなく、なんとなくのイメージを作ってコードを書き始めて、必要が出てきたときには途中でも他のファイルに取り掛かるような感じが合っているような気がします。

明日は、Jarvisがユーザーに話しかけるテンプレートを仕上げたいと思います。

それでは明日もGood Python!