🔙
🔝

【paiza問題集 解説】
クラス・構造体メニュー

構造体の更新

構造体の更新

STEP: 1 構造体の作成

構造体の作成

    class User:
        def __init__(self, nickname, old, birth, state):
            self.nickname = nickname
            self.old = old
            self.birth = birth
            self.state = state
    
    def setup():
        N = int(input())
        datas = [input().split() for _ in range(N)]
        users = [User(*data) for data in datas]
        
        return users
    
    def main(users):
        for user in users:
            print('User{')
            print('nickname :', user.nickname)
            print('old :', user.old)
            print('birth :', user.birth)
            print('state :', user.state)
            print('}')
    
    if __name__ == '__main__':
        main(setup())
    

    ※ 「3章 オブジェクト指向」を修了した前提で進めていきます。

    ただ問題を解くだけではあまり意味がありませんので、paiza の解答コード例を先に参照しつつ、ここではクラス型で解説をしていきます。
    後の本番の予行演習として、実際に自分の手でクラス型に組み換えてみてください。

    まずは最下行にある main() 関数を呼び出す際の引数 setup() 関数から始まります。ここで入力を受け取り、インスタンスを作成しています。そのインスタンスが入ったリストを引数に、 main() 関数を実行します。

    コンストラクタにはクラスのみんなのアカウント情報を格納します。それらを main() 関数内から各ユーザー(user)のインスタンス変数を参照して利用します。

    この状態から main() 関数の中身をメソッド化し、メソッドを呼び出して実行するプログラムに書き換えてみてください。

    オブジェクト指向は、スキルチェックBランク問題でも必要性はありませんが、Cランクに比べて複雑になり、またこのような構造体の問題が頻繁に出題されます。プログラムが複雑化すると後から見返した時に自分で組んだものにも関わらず、わかりにくくなってしまいます。一言で言うと「ぐちゃぐちゃ」です。そんな時にクラス型で組むととてもスッキリと組めるようになるメリットがあります。
    実際に組み慣れないと実感が持てませんが、このメニューを無視せずに最後まで解いてみてください。

STEP: 2 構造体の検索

構造体の検索

    class User:
        def __init__(self, nickname, old, birth, state):
            self.nickname = nickname
            self.old = old
            self.birth = birth
            self.state = state
    
    def setup():
        N = int(input())
        datas = [input().split() for _ in range(N)]
        users = [User(*data) for data in datas]
        K = input()
        
        return users, K
    
    def main(users, K):
        for user in users:
            if user.old == K:
                print(user.nickname)
    
    if __name__ == '__main__':
        main(*setup())
    

    リストや辞書で値を検索するように、クラスでもインスタンス変数を参照して値を検索します。どちらかというとこの構造は辞書に近いでしょうか。

    今度は setup() 関数の戻り値が複数ですので、行末は main(*setup()) と、戻り値を展開するようにします。

STEP: 3 構造体の整列

構造体の整列

    class User:
        def __init__(self, nickname, old, birth, state):
            self.nickname = nickname
            self.old = int(old)
            self.birth = birth
            self.state = state
    
    def setup():
        N = int(input())
        datas = [input().split() for _ in range(N)]
        users = [User(*data) for data in datas]
    
        return users
    
    def main(users):
        users.sort(key=lambda x:x.old)
        for user in users:
            print(user.nickname, user.old, user.birth, user.state)
    
    if __name__ == '__main__':
        main(setup())
    

    lambda がわからないとわかりませんよね。😓

    key=lambda という無名関数を充てています。ここだけの関数を作ってここだけで使用していると思ってもらえればよいです。
    xリストの要素であるインスタンス で、x.oldインスタンス.old に相当します。
    lambda x:x.old は、「 インスタンス.old をターゲットとして users をソートする」という意味になります。

    1つ注意として、これまではただ画面に表示するだけでしたので old は文字列のまま処理していましたが、ソートする際は数値化しないと異なる結果が生まれる場合があります。

    数値 : 10 < 20 < 100
    文字列 : 10 < 100 < 20

    その為、インスタンス変数に渡す時に整数化して渡しています。

FINAL問題 構造体の更新

構造体の更新

    class User:
        def __init__(self, nickname, old, birth, state):
            self.nickname = nickname
            self.old = int(old)
            self.birth = birth
            self.state = state
    
    def change_name(users):
        a, nn = input().split()
        users[int(a)-1].nickname = nn
        
    def setup():
        N, K = map(int, input().split())
        datas = [input().split() for _ in range(N)]
        users = [User(*data) for data in datas]
    
        return users, K
    
    def main(users, K):
        for _ in range(K):
            change_name(users)
    
        for user in users:
            print(user.nickname, user.old, user.birth, user.state)
    
    if __name__ == '__main__':
        main(*setup())
    

    Python式に、関数名を change_name とします。

    生徒の情報がリストの要素番号 0 から始まっていますので、生徒番号 1 から始まる a から 1 を引きます。

    FINAL問題ですので、生徒の名前の書き換えと最終出力をメソッドにしてみましょう。

静的メンバ

静的メンバ

STEP: 1 クラスの作成

クラスの作成

    class Employee:
        def __init__(self, number, name):
            self.number = number
            self.name = name
        
        def get_num(self):
            return self.number
        
        def get_name(self):
            return self.name
    
    def setup():
        N = int(input())
        employees = []
        
        return N, employees
    
    def main(N, employees):
        for _ in range(N):
            S = input().split()
            query, num = S[0], int(S[1])
            
            if query == 'getnum':
                print(employees[num-1].number)
            elif query == 'getname':
                print(employees[num-1].name)
            else:
                employees.append(Employee(num, S[2]))
    
    if __name__ == '__main__':
        main(*setup())
    

    問題の通りに組むと大体こうなります。直接インスタンス変数を参照するのではなく、メソッドを使って社員番号や名前を参照しています。こういう役割りをするメソッドのことを「ゲッター (getter)」と言います。

STEP: 2 コンストラクタ

コンストラクタ

    class Employee:
        def __init__(self, number, name):  # コンストラクタ
            self.number = number
            self.name = name
        
        def get_num(self):
            return self.number
        
        def get_name(self):
            return self.name
    
    def setup():
        N = int(input())
        employees = []
        
        return N, employees
    
    def main(N, employees):
        for _ in range(N):
            S = input().split()
            query, num = S[0], int(S[1])
            
            if query == 'getnum':
                print(employees[num-1].number)
            elif query == 'getname':
                print(employees[num-1].name)
            else:
                employees.append(Employee(num, S[2]))
    
    if __name__ == '__main__':
        main(*setup())
    

    1つ前の問題とプログラムが全く同じになります。

    コンストラクタはインスタンスを作成する際、一番最初に一回だけ実行されるメソッドです。このメソッドでインスタンス変数(self. が付いた変数)を作り、初期化します。その際、引数として受け取った変数をインスタンス変数に渡すことによってインスタンスが所有する値となります。このインスタンス変数のスコープは self の範囲内ですので、self で示されたメソッドなら引数を渡す必要はありません。

    インスタンス変数は受け取った引数でなくても初期化することができます。self.num = 0 の様に。
    また、インスタンス変数は他のメソッド内でも作成することができますが、その場合はコンストラクタで予め値を None などにして初期化しておくとインスタンス変数が管理しやすくなります。

STEP: 3 クラスのメンバの更新

クラスのメンバの更新

    class Employee:
        def __init__(self, number, name):
            self.number = number
            self.name = name
        
        def get_num(self):
            return self.number
        
        def change_num(self, number):
            self.number = number
        
        def get_name(self):
            return self.name
        
        def change_name(self, name):
            self.name = name
    
    def setup():
        N = int(input())
        employees = []
        
        return N, employees
    
    def main(N, employees):
        for _ in range(N):
            S = input().split()
            query, num = S[0], int(S[1])
            
            if query == 'getnum':
                print(employees[num-1].number)
            elif query == 'getname':
                print(employees[num-1].name)
            elif query == 'change_num':
                employees[num-1].change_num(S[2])
            elif query == 'change_name':
                employees[num-1].change_name(S[2])
            else:
                employees.append(Employee(num, S[2]))
    
    if __name__ == '__main__':
        main(*setup())
    

    プログラムがどんどん長くなっていきますが、今回の変更追加箇所は change_numchange_name の分です。インスタンスが持つ値を参照する為だけのメソッドが ゲッター に対し、インスタンスが持つ値を変更する為だけのメソッドのことを 「セッター (setter)」 と言います。二つ合わせて ゲッターセッター なんて呼ばれていますが、天気予報はしませんし田畑を耕す能力もありません。

STEP: 4 クラスの継承

クラスの継承

    class Child:
        def __init__(self):
            self.total = 0
    
        def serve_softdrink(self, m):
            self.total += m
            
        def serve_food(self, m):
            self.total += m
    
        def serve_alcohol(self, m):
            pass
        
        def get_total(self):
            return self.total
    
    class Adult(Child):
        def __init__(self):
            super().__init__()
            self.alcohol = False
        
        def serve_food(self, m):
            self.total += m
            if self.alcohol:
                self.total -= 200
    
        def serve_alcohol(self, m):
            self.total += m
            self.alcohol = True
    
    
    def setup():
        N, K = map(int, input().split())
        customers = [Child() if int(input()) < 20 else Adult() for _ in range(N)]
        
        return K, customers
    
    def main(K, customers):
        for _ in range(K):
            n, s, m = [[int(n), s, int(m)] for n, s, m in [input().split()]].pop()
            
            if s == 'food':
                customers[n-1].serve_food(m)
            elif s == 'softdrink':
                customers[n-1].serve_softdrink(m)
            else:
                customers[n-1].serve_alcohol(m)
        
        for customer in customers:
            print(customer.get_total())
        
    if __name__ == '__main__':
        main(*setup())
    

    オブジェクト指向の重要な要素の1つである 継承けいしょう についての問題です。

    大元のクラスは class Child: です。ややこしいですが、これが「親クラス」となります。
    class Adult(Child): クラスは、親クラスである class Child: がコピーされた「子クラス」となります。これが 継承 です。継承したからには親クラスとはまた異なる特徴を持つべきものにならないと継承の意味がありません。継承した子クラスである Adult ができて Child ができないこと。それが「酒の注文」です。

    Child クラスでは酒の注文を受けても無視されます。その為、serve_alcohol() メソッドでは pass になっています。一言も言わず完全無視です。
    一方、Adult クラスでは注文を受け付けています。

    serve_food() メソッドは、酒類が注文できない Child クラスでは食事の割引きサービスを受けることはできません。一方 Adult クラスでは、酒類の注文歴があればその後に注文した食事から 200円引きされます。

    もう1つソフトドリンクの注文は、大人も子どももサービスに差はありません。その為、Adult クラスに serve_softdrink() メソッドが書かれていませんが、これは親クラスの serve_softdrink() メソッドをそのまま継承している為、記述は不要です。

    逆に子クラスで違う内容でもう一度同じメソッドが書かれてますが、これは継承した親クラスのメソッドを、子クラスでまるごと上書き(再定義)しています。これを「オーバーライド (override)」といいます。(または「オーバライド」)

    1. 一旦、親クラスを丸ごと継承する
    2. 子クラスのコンストラクタに super().__init__() を追加する
      「親クラスの __init__() メソッド」を実行する…というより、継承するという意味になります。
    3. 追加変更するメソッドを書く

    基本の流れはこんな感じです。1 と 2 の書き方に慣れれば難しいことはありません。が、使わないと忘れます。😅

    インスタンスの作成は、年齢が二十歳未満かどうかで作るクラスを選択してリストの要素に配置しています。

    最後に客の会計結果を画面に出力して完了です。get_total() メソッドも作ってあります。

STEP: 5 デフォルト引数

デフォルト引数

    class Child:
        def __init__(self):
            self.total = 0
    
        def serve_softdrink(self, price):
            self.total += price
            
        def serve_food(self, price):
            self.total += price
    
        def serve_alcohol(self, price=0):
            pass
        
        def get_total(self):
            return self.total
    
    class Adult(Child):
        def __init__(self):
            super().__init__()
            self.alcohol = False
        
        def serve_food(self, price):
            self.total += price
            if self.alcohol:
                self.total -= 200
    
        def serve_alcohol(self, price=500):
            self.total += price
            self.alcohol = True
    
    
    def setup():
        N, K = map(int, input().split())
        customers = [Child() if int(input()) < 20 else Adult() for _ in range(N)]
        
        return K, customers
    
    def main(K, customers):
        for _ in range(K):
            tmp = input().split()
            if len(tmp) == 3:
                n, s, m= [[int(n), s, int(m)] for n, s, m in [tmp]][0]
            else:  # ビールの注文時のみ
                n, s = [[int(n), s] for n, s in [tmp]][0]
    
            if s == 'food':
                customers[n-1].serve_food(m)
            elif s == 'softdrink':
                customers[n-1].serve_softdrink(m)
            elif s == '0':
                customers[n-1].serve_alcohol()
            else:
                customers[n-1].serve_alcohol(m)
        
        for customer in customers:
            print(customer.get_total())
        
    if __name__ == '__main__':
        main(*setup())
    

    デフォルト引数がわからない方は「3章 関数」で先に学習しておいてください。
    main() 関数内にある標準入力方法については 「3章 標準入力」をご覧ください。

    デフォルト引数とは、引数が与えられなかった時に予め用意された初期値が使われる機能です。「3章 関数」では「キーワード引数」として説明しています。そのキーワード引数を省略して書いた時、予め用意しておいた初期値を使って関数が処理されます。

    『何でこんなところでデフォルト引数が?』と思ったのですが、そういえば paiza の問題集は関数に関した問題が用意されていないなと気付きました。コンパイラ言語は関数で書くのが半ば当たり前になっているので、わざわざ関数の問題集を用意する必要は無いと判断してなのか、インタープリタ言語である Python3 の場合は関数は自分で意識して作らないと使わないので、手続き型とオブジェクト指向の間にある関数型がすっ飛んでいる様子に感じてしまいました。

    関数の書き方がわかるとメソッドの書き方もわかるようになりますので、関数の書き方がまだよくわからない、慣れていない方は「3章 関数」で学習してみてください。

    Child クラスのデフォルト引数が 0 になっていますが、メソッドの中身が pass になっていますので、paiza のコード例の様に 500 であっても、使わないからという理由で None としてもプログラムは正常に動作します。ただしここを省略してしまうと、未成年者が酒を注文した時に引数の数が合わなくなり、エラーとなりますので省略はできません。

FINAL問題 静的メンバ

静的メンバ

    class Customer:
        num = 0
    
        def __init__(self):
            self.total = 0
    
        def serve_softdrink(self, price):
            self.total += price
            
        def serve_food(self, price):
            self.total += price
    
        def serve_alcohol(self, price=0):
            pass
        
        def get_total(self):
            return self.total
        
        def paid_total(self):
            print(self.get_total())
            Customer.num += 1
    
    class Adult(Customer):
        def __init__(self):
            super().__init__()
            self.alcohol = False
        
        def serve_food(self, price):
            self.total += price
            if self.alcohol:
                self.total -= 200
    
        def serve_alcohol(self, price=500):
            self.total += price
            self.alcohol = True
    
    
    def setup():
        N, K = map(int, input().split())
        customers = [Customer() if int(input()) < 20 else Adult() for _ in range(N)]
        
        return K, customers
    
    def main(K, customers):
        for _ in range(K):
            tmp = input().split()
            if len(tmp) == 3:
                n, s, m= [[int(n), s, int(m)] for n, s, m in [tmp]][0]
            else:
                n, s = [[int(n), s] for n, s in [tmp]][0]
    
            if s == 'food':
                customers[n-1].serve_food(m)
            elif s == 'softdrink':
                customers[n-1].serve_softdrink(m)
            elif s == '0':
                customers[n-1].serve_alcohol()
            elif s == 'A':
                customers[n-1].paid_total()
            else:
                customers[n-1].serve_alcohol(m)
        
        print(Customer.num)
    
    if __name__ == '__main__':
        main(*setup())
    

    親クラス名を Customer に変更しました。

    今度はインスタンスからはみ出た変数が1つ、num = 0 があります。これはインスタンスではなく、クラスが持つ変数なので クラス変数 と言います。このクラス変数はすべてのインスタンスから参照でき、変更することができる変数です。
    アクセスする時は クラス名.変数 と書いてアクセスします。他のクラスからもアクセスできるので、今回の Customer クラスを継承した Adult クラスからもアクセスすることができますし、インスタンス変数同様に、クラスの外からも同様の書き方でアクセスできます。

ロボットの暴走

ロボットの暴走

STEP: 1 出口のない迷路

STEP: 1 出口のない迷路

    class Wordpoint:
        def __init__(self, word, num1, num2):
            self.word = word
            self.num  = [num1, num2]
            
        def get_word(self):
            return self.word
        
        def get_next(self, num):
            return self.num[num-1]
    
    def setup():
        # N = 地点の数 , K = 移動回数 , S = 移動を開始する地点の番号
        N, K, S = map(int, input().split())
        wordpoints = [Wordpoint(a, int(r1), int(r2)) for _ in range(N) for a, r1, r2 in [input().split()]]
        np = S  # np = now point のこと
        password = wordpoints[np-1].get_word()  # 開始地点の文字を取得し、初期値とする
        
        return K, wordpoints, np, password
    
    def main(K, wordpoints, np, password):
        for _ in range(K):
            M = int(input())
            np = wordpoints[np-1].get_next(M)
            password += wordpoints[np-1].get_word()
            
        print(password)
    
    if __name__ == '__main__':
        main(*setup())
    

    問題文をざっと読んで、何が言いたいのかよくわからなかった時は入力例を見て察しましょう。😅

    各ワードポイントは 道順 1 (r1)道順 2 (r2) に分岐していて、r1 と r2 に記された次のワードポイントに繋がっています。
    次のワードポイントは 入力 M から得られます。この 入力 M が指示する道順に従って進んでいくと、やがて唱えるべき呪文が完成するという流れです。

    ここではワードポイントをクラスにしています。各ワードポイントの情報をインスタンスに持たせ、各インスタンスにアクセスして、文字や次に繋がるワードポイントを得ていきます。

    ちょっと混乱するところもありますが、 setup() 関数と main() 関数の役割をしっかり分離して書くことができれば、混乱も軽減されるはずです。

STEP: 2 RPG

RPG

    class Hero:
        def __init__(self, l, h, a, d, s, c, f):
            self.LEVEL = l  # level
            self.HP = h     # HP
            self.ATK = a    # attack
            self.DEF = d    # defense
            self.AGI = s    # agility じゃなくて speed
            self.INT = c    # intelligence じゃなくて clever
            self.LUK = f    # *uck じゃなくて fotune
        
        def levelup(self, h, a, d, s, c, f):
            self.LEVEL += 1
            self.HP += h
            self.ATK += a
            self.DEF += d
            self.AGI += s
            self.INT += c
            self.LUK += f
            
        def muscle_training(self, h, a):
            self.HP += h
            self.ATK += a
    
        def running(self, d, s):
            self.DEF += d
            self.AGI += s
        
        def study(self, c):
            self.INT += c
        
        def pray(self, f):
            self.LUK += f
            
        def output_status(self):
            print(self.LEVEL, self.HP, self.ATK, self.DEF, self.AGI, self.INT, self.LUK)
    
    
    def setup():
        N, K = map(int, input().split())
        params = [list(map(int, input().split())) for _ in range(N)]
        heroes = [Hero(*param) for param in params]
    
        return K, heroes
        
    def main(K, heroes):
        for _ in range(K):
            to, event, p = input().split(maxsplit=2)
            to = int(to) - 1
            
            if event == 'levelup':
                h, a, d, s, c, f = map(int, p.split())
                heroes[to].levelup(h, a, d, s, c, f)
            elif event == 'muscle_training':
                h, a = map(int, p.split())
                heroes[to].muscle_training(h, a)
            elif event == 'running':
                d, s = map(int, p.split())
                heroes[to].running(d, s)
            elif event == 'study':
                c = int(p)
                heroes[to].study(c)
            elif event == 'pray':
                f = int(p)
                heroes[to].pray(f)
    
        for hero in heroes:
            hero.output_status()
    
    if __name__ == '__main__':
        main(*setup())
    

    必然的にプログラムが長くなってしまいますが、各メソッドのステータスの変化とイベント処理が長いだけで構造は単純です。ただ、クエリ処理が面倒ですね。😓

    イベントの受け取りの 入力 to入力 event は場所が固定されていて、それ以降の入力は可変となっています。ですので、.split() メソッドの引数に maxsplit=2 を与えてそれ以降の入力値を文字列として 変数 p に仮として受け取っておきます。

    この変数 p をさらに分割して相当する各変数に格納します。その各変数を引数としてメソッドを呼び出して処理をします。
    これを繰り返してループを抜けたら結果を出力して完了です。

    変数 p の分割処理をメソッド内で行なうこともできます。

    class Hero:
        def __init__(self, l, h, a, d, s, c, f):
            self.LEVEL = l
            self.HP = h
            self.ATK = a
            self.DEF = d
            self.AGI = s
            self.INT = c
            self.LUK = f
        
        def levelup(self, p):
            h, a, d, s, c, f = map(int, p.split())  # ← ココ
            self.LEVEL += 1
            self.HP += h
            self.ATK += a
            self.DEF += d
            self.AGI += s
            self.INT += c
            self.LUK += f
            
        def muscle_training(self, p):
            h, a = map(int, p.split())  # ← ココ
            self.HP += h
            self.ATK += a
    
        def running(self, p):
            d, s = map(int, p.split())  # ← ココ
            self.DEF += d
            self.AGI += s
        
        def study(self, c):
            self.INT += int(c)  # 引数はそのまま使える
        
        def pray(self, f):
            self.LUK += int(f)  # 引数はそのまま使える
            
        def output_status(self):
            print(self.LEVEL, self.HP, self.ATK, self.DEF, self.AGI, self.INT, self.LUK)
    
    
    def setup():
        N, K = map(int, input().split())
        params = [list(map(int, input().split())) for _ in range(N)]
        heroes = [Hero(*param) for param in params]
    
        return K, heroes
        
    def main(K, heroes):
        for _ in range(K):
            num, event, p = input().split(maxsplit=2)
            num = int(num) - 1
            
            if event == 'levelup':
                heroes[num].levelup(p)
            elif event == 'muscle_training':
                heroes[num].muscle_training(p)
            elif event == 'running':
                heroes[num].running(p)
            elif event == 'study':
                heroes[num].study(p)
            elif event == 'pray':
                heroes[num].pray(p)
    
        for hero in heroes:
            hero.output_status()
    
    if __name__ == '__main__':
        main(*setup())
    

    イベント名とメソッド名が一致する書き方をしているので、 getattr() という関数を使うと 文が不要になり、 main() 関数がすっきりします。

    class Hero:
        def __init__(self, l, h, a, d, s, c, f):
            self.LEVEL = l
            self.HP = h
            self.ATK = a
            self.DEF = d
            self.AGI = s
            self.INT = c
            self.LUK = f
        
        def levelup(self, p):
            h, a, d, s, c, f = map(int, p.split())
            self.LEVEL += 1
            self.HP += h
            self.ATK += a
            self.DEF += d
            self.AGI += s
            self.INT += c
            self.LUK += f
            
        def muscle_training(self, p):
            h, a = map(int, p.split())
            self.HP += h
            self.ATK += a
    
        def running(self, p):
            d, s = map(int, p.split())
            self.DEF += d
            self.AGI += s
        
        def study(self, c):
            self.INT += int(c)
        
        def pray(self, f):
            self.LUK += int(f)
            
        def output_status(self):
            print(self.LEVEL, self.HP, self.ATK, self.DEF, self.AGI, self.INT, self.LUK)
    
    
    def setup():
        N, K = map(int, input().split())
        params = [list(map(int, input().split())) for _ in range(N)]
        heroes = [Hero(*param) for param in params]
    
        return K, heroes
        
    def main(K, heroes):
        for _ in range(K):
            num, event, p = input().split(maxsplit=2)
            getattr(heroes[int(num)-1], event)(p)  # ← ココ
    
        for hero in heroes:
            hero.output_status()
    
    if __name__ == '__main__':
        main(*setup())
    
  • getattr(オブジェクト, str)

    str
    「文字列 = 関数名」
    「文字列 = メソッド名」

    と、なること。

  • 今回の例で言うと、オブジェクトは heroes要素 num のインスタンスです。
    str はそのインスタンスのメソッド名と同じ文字列であること。
    最後に引数を、無ければ () だけを付ければ正常に処理できるはずです。

    この方法を使えば、どんなにイベントの数が多かろうとこの一文だけで済んでしまいます。イベント名とメソッド名を一致させることだけ注意すれば、この方法のほうが簡単に思えてくるでしょう。

    ステータスをリストにすると、少しプログラムが短くなります。

    class Hero:
        def __init__(self, l, h, a, d, s, c, f):
            self.status = [l, h, a, d, s, c, f]
    
        def levelup(self, p):
            params = [1] + list(map(int, p.split()))
            for i, param in enumerate(params):
                self.status[i] += param
    
        def muscle_training(self, p):
            h, a = map(int, p.split())
            self.status[1] += h
            self.status[2] += a
    
        def running(self, p):
            d, s = map(int, p.split())
            self.status[3] += d
            self.status[4] += s
        
        def study(self, c):
            self.status[5] += int(c)
        
        def pray(self, f):
            self.status[6] += int(f)
            
        def output_status(self):
            print(*self.status)
    
    
    def setup():
        N, K = map(int, input().split())
        params = [list(map(int, input().split())) for _ in range(N)]
        heroes = [Hero(*param) for param in params]
    
        return K, heroes
        
    def main(K, heroes):
        for _ in range(K):
            num, event, p = input().split(maxsplit=2)
            getattr(heroes[int(num)-1], event)(p)
    
        for hero in heroes:
            hero.output_status()
    
    if __name__ == '__main__':
        main(*setup())
    
STEP: 3 格闘ゲーム

格闘ゲーム

    class Player:
        num = 0
        
        def __init__(self, hp, f1, a1, f2, a2, f3, a3):
            self.hp = hp
            self.f_i = [f1, f2, f3]
            self.a_i = [a1, a2, a3]
            Player.num += 1
    
        def damaged(self, dmg):
            self.hp = max(self.hp - dmg, 0)  # HP が 0 未満のときは 0 にする
            Player.num -= not self.hp
        
        def powerup(self):
            for i in range(3):
                if self.f_i[i] > 0:
                    self.f_i[i] = max(self.f_i[i] - 3, 1)
                    self.a_i[i] += 5
        
        def get_status(self, p):
            return self.f_i[p], self.a_i[p]
        
        def is_playing(self):
            return self.hp
    
    
    def setup():
        N, K = map(int, input().split())
        params = [list(map(int, input().split())) for _ in range(N)]
        players = [Player(*param) for param in params]
    
        return K, players
    
    def main(K, players):
        for _ in range(K):
            P1, T1, P2, T2 = [tmp-1 for tmp in map(int, input().split())]
    
            p_1, (f_1, a_1) = players[P1], players[P1].get_status(T1)
            p_2, (f_2, a_2) = players[P2], players[P2].get_status(T2)
            
            if p_1.is_playing() and p_2.is_playing():  # 両方とも場に残っているとき
                if f_1 == 0:
                    p_1.powerup()
                    f_1 = float('inf')
                if f_2 == 0:
                    p_2.powerup()
                    f_2 = float('inf')
    
                if f_1 < f_2:
                    p_2.damaged(a_1)
                elif f_1 > f_2:
                    p_1.damaged(a_2)
                else:  # 省略可
                    pass
        
        print(Player.num)
    
    
    if __name__ == '__main__':
        main(*setup())
    

    長いですね。😓

    まずクラスのターゲットは「プレイヤー」です。インスタンスを作ってプレイヤーを増やします。そのプレイヤーの特徴として、

    『各プレイヤーは 決まった hp と 3 種類の技を持っていて、技には強化系の技と攻撃の技があり、各攻撃技には技を出すための発生フレーム F とダメージ A が設定されている。』

    とありますので、「プレイヤーのHP」「技の発生フレーム」「ダメージ(攻撃力)」をコンストラクタに用意します。

    次に、クラス変数には現在のプレイヤー参加人数である num を用意し、インスタンスが作られる毎にこれを Player.num += 1 で +1 していきます。

    次に、複数いるプレイヤーから「プレイヤー 1」と「プレイヤー 2」が選ばれ、それぞれが使用する技と共に入力で与えられ、闘います。これらを使ってダメージ判定を行ないます。

    まず『どちらか片方でもプレイヤーが退場している場合、互いに何も起こらない。』とあります。これは逆に言えば「二人ともゲーム続行可能なら闘う」ということです。コメントの所です。paiza の解答コード例では not を使って問題文のとおりに書かれています。

    ここで謎なことがあります。「入力される値」のところに、

    発生フレーム・攻撃力が共に 0 である技は強化技であることを表しています。

    とありますが、問題文には、

    強化系の技を使った場合、使ったプレイヤーの他の全ての技の発生フレーム(最短 1 フレーム) を -3 , 攻撃力を +5 する。

    とあります。実際 paiza の解答コード例には

    # paiza 解答コード例から引用
    def enhance(self):
        for i in range(3):
            if self.f[i] == 0 and self.a[i] == 0:
                continue
    
            self.f[i] = max(self.f[i] - 3, 1)  # ← ココ
            self.a[i] += 5
    # 引用終わり
    

    と、最短フレームが 1 に設定されています。これはもし、0 フレームで攻撃できる攻撃力が 1 以上の技があった場合でも、強化系の技を使用するとフレームが 0 → 1 になってしまうということになります。

    ですので、0 フレームで攻撃できる技があるとは考えにくく、「 0 フレームの時は必ず攻撃力も 0 である」と考えられます。

    そうすると、わざわざフレーム数と攻撃力が共に 0 であるという条件式でなくても、

    if f_1 == 0:
        p_1.powerup()
        f_1 = float('inf')
    if f_2 == 0:
        p_2.powerup()
        f_2 = float('inf')
    

    というように、フレーム数だけで判定しても問題ないはずです。

    def damaged(self, dmg):
        self.hp = max(self.hp - dmg, 0)  # HP が 0 未満のときは 0 にする
        Player.num -= not self.hp
    

    この部分はダメージ計算をするメソッドなのですが、問題のルールに、

    『hp が 0 になったプレイヤーは退場となる。』

    とありますので、HP をマイナスにしたままにせず、最小値を 0 にします。これは実際にどんなゲームでも体力がマイナス表記されることはなく、必ず 0 と表記されるからというちょっとしたこだわりを入れてみたというのもありますが、上記プログラムの三行目の「HP が 0 の時、not で反転して True、つまり 1 にする」ことでプレイヤー人数を減らすということもしています。HP が 1 以上の時は False、つまり 0 となりますのでプレイヤー人数はそのままです。

    そして HP の値がそのままゲーム続行中かどうかの判定にも使えるようになります。

    def is_playing(self):
        return self.hp
    
    if p_1.is_playing() and p_2.is_playing():  # 両方とも場に残っているとき
    

    文章っぽくなりましたね。イイカンジ😉

    それとこの部分なのですが、

    def get_status(self, p):
        return self.f_i[p], self.a_i[p]
    
    p_1, (f_1, a_1) = players[P1], players[P1].get_status(T1)
    p_2, (f_2, a_2) = players[P2], players[P2].get_status(T2)
    

    .get_status()メソッドの戻り値はタプルとなって返ります。このついでに players[P1]p_1 に名前を短縮して読みやすくしてしまおうとこの様な書き方をしたのですが、タプルで返ってくる部分の変数は ( ) で囲わないとエラーになってしまいます。これは、

    for i, (k, v) in enumerate(dic.items()):
    

    という書き方と同じことです。

    最後にプレイヤー人数を画面に出力して完了です。

    この考え方、書き方で本当に正しいのかどうか、100点を取ってももやもやが晴れない感じが気持ち悪いのですが、正しいかどうかに関わらず、なにかしら参考になれば幸いです。

STEP: 4 スーパースーパースーパーカー

スーパースーパースーパーカー

    class Supercar:
        def __init__(self, fuel, f):
            self.fuel = fuel
            self.f = f
            self.km = 0
    
        def run(self):
            if self.fuel:
                self.fuel -= 1
                self.km += self.f
        
        def get_km(self):
            return self.km
    
    class Supersupercar(Supercar):
        def __init__(self, fuel, f):
            super().__init__(fuel, f)
            self.moving_fly = self.f ** 2
            
        def fly(self):
            if self.fuel >= 5:
                self.fuel -= 5
                self.km += self.moving_fly
            else:
                self.run()
    
    class Supersupersupercar(Supersupercar):
        def __init__(self, fuel, f):
            super().__init__(fuel, f)
            self.moving_fly = 2 * self.f ** 2
    
        def teleport(self):
            if self.fuel >= self.f ** 2:
                self.fuel -= self.f ** 2
                self.km += self.f ** 4
            else:
                self.fly()
    
    
    def setup():
        N, K = map(int, input().split())
        cars = [globals()[k.capitalize()](int(l), int(f)) for _ in range(N) for k, l, f in [input().split()]]
        return K, cars
    
    def main(K, cars):
        for _ in range(K):
            n, func = input().split()
            getattr(cars[int(n)-1], func)()
        
        for car in cars:
            print(car.get_km())
    
    
    if __name__ == '__main__':
        main(*setup())
    

    継承してさらに継承していますね。親クラス → 子クラス → 孫クラス となっています。

    子クラス・孫クラスのコンストラクタで親クラスのコンストラクタを継承しています。

    def __init__(self, fuel, f):
        super().__init__(fuel, f)
    

    親クラスのコンストラクタを継承し、インスタンスを作成する際に受け取った引数を、継承したコンストラクタへ渡して初期化しています。

    ここからはプログラムを少しでも短くするために特殊な方法で書いた所を説明します。

    cars = [
        globals()[k.capitalize()](int(l), int(f))
        for _ in range(N)
        for k, l, f in [input().split()]
    ]
    

    横に長いので複数行に分割しましたが、2行目に注目してください。これは getattr() と同じ様なことをしているのですが、グローバルスコープではなぜか getattr() が使えず、代わりに globals() 関数を使いました。(getattr() が使えないのではなく、私の知識不足で本当はやり方があるのかもしれません😓)

    globals() 関数はグローバルな位置にある関数などを辞書に集めてくれる機能ですが、言葉だけではよくわからないと思いますので実際に見える化してみましょう。

    # スーパースーパースーパーカーのプログラムは全省略
    print(globals())
    
    {'__name__': '__main__',
    '__doc__': None,
    '__package__': None,
    '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x400036adb130>,
    '__spec__': None,
    '__annotations__': {},
    '__builtins__': <module 'builtins' (built-in)>,
    '__file__': 'Main.py',
    '__cached__': None,
    'Supercar': <class '__main__.Supercar'>,
    'Supersupercar': <class '__main__.Supersupercar'>,
    'Supersupersupercar': <class '__main__.Supersupersupercar'>,
    'setup': <function setup at 0x400036ab61f0>,
    'main': <function main at 0x400036c03f70>}

    このような形で辞書に格納されて返ってきます。ここで使いたいのは Supercar Supersupercar Supersupersupercar の3つのクラスです。
    辞書になっているので使い方は辞書と同じで、 getattr()strglobals()キーにあたります。ですので、入力で取得したクラス名を添字に当てればクラスに変換されます。

    例)k = 'supersupercar'

    globals()[k.capitalize()](int(l), int(f))
    
    globals()['supersupercar'.capitalize()](int(l), int(f))
    
    globals()['Supersupercar'](int(l), int(f))
    
    <class '__main__.Supersupercar'>(int(l), int(f))  # インスタンスが作られる
    # Supersupercar(int(l), int(f)) と同等
    

    .capitalize() メソッドは文字列の先頭の一文字を大文字に変換する機能です。 .upper() メソッドなどの仲間です。クラス名は先頭の文字を大文字にするという慣習に合わせる為にこの処理をしています。

    一見難しそうでしたけど、わかれば大したことないですよね。😉
    ただ、一行 79文字以下に合わせると結局行数が増えてしまいますが。😅

    main() 関数内の getattr() も上記と同様の目的で書いたものです。
    クエリがテキストで受け取れる場合はこの方法を使えばやたら長い 文を省略できますし、もしクエリを数字などで受け取る場合や、受け取った文字列と関数名が異なる場合でも、辞書のキーと値を使って変換してしまえば問題ありません。

    例)func = '0'

    trans_func = {'0': 'run', '1': 'fly', '2': 'teleport'}
    getattr(cars[int(n)-1], trans_func[func])()
    

    ただ、わかりやすいかと問われても胸張って断言できませんけどね。😅

FINAL問題 ロボットの暴走

ロボットの暴走

    class Robot:
        DIRECTIONS = {'N': (0, -1), 'S': (0, 1), 'W': (-1, 0), 'E': (1, 0)}
        STEPS = (1, 2, 5, 10)
        TOOLBOX_LOCS = None  # 工具箱の位置
    
        def __init__(self, x, y, level):
            self.x = x
            self.y = y
            self.level = level
    
        def move(self, direction):
            dx, dy = Robot.DIRECTIONS[direction]
            step = Robot.STEPS[self.level - 1]
            self.x += dx * step
            self.y += dy * step
            self.level = self.check_level_up()
    
        def check_level_up(self):
            current_position = (self.x, self.y)
            if current_position in Robot.TOOLBOX_LOCS:
                return min(self.level + 1, 4)
            else:
                return self.level
    
        def print_info(self):
            print(self.x, self.y, self.level)
    
    
    def setup():
        _, _, N, K = map(int, input().split())  # H, W = (使わない)
    
        toolbox_num = 10
        Robot.TOOLBOX_LOCS = [tuple(map(int, input().split())) for _ in range(toolbox_num)]
        robots = [Robot(x, y, l) for _ in range(N) for x, y, l in [map(int, input().split())]]
        queries = [input().split() for _ in range(K)]
        
        return robots, queries
    
    
    def main(robots, queries):
        for r, d in queries:  # 暴走
            robots[int(r) - 1].move(d)
    
        for robot in robots:  # 結果出力
            robot.print_info()
    
    
    if __name__ == '__main__':
        main(*setup())
    

    ここまで苦労してでもなんとかクリアしてきた方ならこの問題がクリアできなかったとしても、解説無しで読めるようになっているはずです。ですので、補足説明だけにさせていただきます。

    標準入力と初期化を setup() 関数に集めているのはこれまでと同じです。クラス変数に、ロボットの移動情報 DIRECTIONS、ロボットのレベルに応じた移動マス STEPS、工具箱が置いてある座標 TOOLBOX_LOCS を用意します。この時、TOOLBOX_LOCSsetup() 関数で入力された値(リスト)をクラス変数に渡す為、値を None にして予めクラス変数に明記しておいています。これは書かなくても動作に支障はありません。

    DIRECTIONS は、受け取った方向の情報から移動すべき方向の座標が記してあります。この値にレベルに応じた移動マスを掛け算してロボットが移動します。
    この DIRECTIONS の書き方や使い方は使う場面が結構あります。何かが四方八方に移動したり、現在地から各方向に障害物があるかどうかをチェックしに行ったりする際には、この辞書の書き方を知っておくと楽にプログラムが書けるようになります。八方向なら8つ書きます。
    Bランクのスキルチェックでも度々出てきますし、問題集でも他のところで出てきますので、形だけでもほんのり覚えておいてください。

    move() メソッド内で、 check_level_up() メソッドを呼び出しています。同じインスタンスから別のメソッドを呼び出す時は、メソッド名の前に self. を付けると呼び出すことができます。
    paiza の解答コード例ではメインプログラムから level_up() メソッドを呼び出していますが、クラス内でできる事はすべてクラスにやらせてしまえというのがオブジェクト指向の考え方だそうです。
    材料を setup() したら、 main() ボタンをポチりと押すだけであとはクラスがすべてやってくれる組み方が望ましいということです。このメニューでは出てきませんでしたが、クラスメソッド というのを使うとさらに実現しやすくなります。

    上記のプログラム例も最高と呼べる状態にはなっていません。これから先、クラス型でプログラムを組み、改善を繰り返していくうちに何がより良いか、その感覚がわかってきます。ChatGPTなどの対話型 AI に相談したりして考えてみてください。

    ちなみに私の経験上、ネットの情報も書籍もほとんど役には立ちませんでした。結局は自分で組んだプログラムをクラス型に変換しまくりながら改善しまくるのが理解への唯一の方法でした。その第一歩としてここで基礎の一部を学んだわけです。

    クラス型で美コードが書けると気持ちがいいですよ。😊 これまで紹介してきたプログラム例もまだ改善の余地があります。もう少しクラス型に触れてみたいなら、ぜひこれらのプログラムや paiza の解答コード例を書き換えたり、または参考にして自身のプログラムを書き換えたり、一から組み直してみたりして自分なりの美コードを作ってみてください。😽