はじめに
オブジェクト指向をより実践的に理解するため、ある課題を題材にして、Pythonのコードを「関数ベース → クラス設計」へとリファクタリングしていく手順をまとめました
間違いや改善点等あれば、ぜひお願いします!
対象読者
- プログラミングを学び始めた方
- Pythonの知識を深めたい方
- オブジェクト指向ってなんだ?と迷子な方
- AIに頼りすぎて、自分のコードがわからなくなってしまった方(←私ww)
※ 細かいPythonの書き方については解説しません
オブジェクト指向とは
データと処理を「オブジェクト」というひとつのまとまりとして扱う考え方
コードの再利用性、保守性、拡張性を高める
基本要素
- クラス:オブジェクトを作成するための「設計図」のこと。同じクラスから複数のオブジェクト(インスタンス)を生成できる(よくたい焼きの「型」にたとえられている)
- プロパティ:オブジェクトが持つデータや状態(たい焼きの中身)
- メソッド:オブジェクトが持つデータを操作する関数(処理)のこと
- インスタンス:クラスから生成された実際のオブジェクト(たとえでいうたい焼き)
- カプセル化:データの構造とそのデータを操作するメソッドをまとめ、外部からの直接的なアクセスを制御する概念
- 継承:ある親クラスの機能を子クラスが引き継ぐ仕組み(クラス同士がis aの関係)。これを使用すると共通機能を何度も書く必要がなくなり、コードの再利用性や拡張性が高まる
- コンポジション:オブジェクトが別のオブジェクトを部品として持つ仕組み(has aの関係)
課題
下記の課題を題材にどうやってコードを書くか考えます!
要約: 複数クラスに所属する生徒の成績データ(生徒名と点数)を受け取り、クラスごとと学校全体の平均点を計算して表示するプログラムを作成する。
詳細:各クラスには複数の生徒が在籍しており、生徒は「名前」と「点数(数値)」を持つ。
各クラスごとに生徒の得点を表示し、クラス平均を計算して表示する。
全クラスの処理が終わったら、学校全体の平均点を計算・表示する。
classes = {
"1年A組": {"佐藤": 85, "鈴木": 92, "高橋": 78},
"1年B組": {"田中": 88, "渡辺": 76, "山本": 90, "中村": 85},
"1年C組": {"伊藤": 80, "山田": 83}
}
1年A組の成績処理を開始します
佐藤: 80
鈴木: 92
高橋: 78
1年A組の平均点: 85.00点
(以下略)
学校全体の平均点: 83.75点
ということで、まずは関数ベースで書いてみる
def process_grades_and_calculate_average(classes_data):
school_total = 0 # 学校全体の点数合計
school_count = 0 # 学校全体の生徒数
class_results = {} # 各クラスの結果を格納
for class_name, students in classes_data.items():
class_total = 0
class_count = 0
# クラス内の各生徒の点数を集計
for score in students.values():
class_total += score
class_count += 1
# クラスの平均点を計算
class_average = class_total / class_count if class_count > 0 else 0
# 学校全体の集計に加算
school_total += class_total
school_count += class_count
# クラス結果を格納
class_results[class_name] = {
'total': class_total,
'count': class_count,
'average': class_average
}
# 学校全体の平均点を計算
school_average = school_total / school_count if school_count > 0 else 0
# すべての結果をまとめて返す
return {
'school_total': school_total,
'school_count': school_count,
'school_average': school_average,
'class_results': class_results
}
def main():
# データ
classes = {
"1年A組": {"佐藤": 85, "鈴木": 92, "高橋": 78},
"1年B組": {"田中": 88, "渡辺": 76, "山本": 90, "中村": 85},
"1年C組": {"伊藤": 80, "山田": 83}
}
# 集計処理を実行
results = process_grades_and_calculate_average(classes)
# --- クラス別の結果表示 ---
for class_name, students in classes.items():
class_result = results['class_results'][class_name]
print(f"\n{class_name}の成績処理を開始します")
# 生徒名と点数の表示
for student, score in students.items():
print(f"{student}: {score}")
# 平均点の表示
print(f"{class_name}の平均点: {class_result['average']:.2f}")
# --- 学校全体の結果表示 ---
print(f"\n学校全体の平均点: {results['school_average']:.2f}")
# プログラムの実行
if __name__ == "__main__":
main()
上記コードで動きはいますが、下記の点からクラスを用いる必要性がありそうです
- データの検証不足
classesの点数に文字やマイナスの値が入力されても計算するまでエラーを感知できない - ロジックの分離不足
main関数でデータ取得と、コンソール出力の異なる役割を担っていて、JSON形式で出力したくなった場合にこの関数全体を書き直す必要がある
これを解消するためにオブジェクト指向の設計をしてみます~
タスク分解
まず、やることを洗い出し
| ステップ | やること |
|---|---|
| 1 | 入出力の形を決める |
| 2 | 登場するものを洗い出す |
| 3 | 各クラスの責務を決める |
| 4 | クラス間の関係を整理する |
| 5 | getter/setterを使うデータを選ぶ(カプセル化) |
1. 入出力の形を決める
- 入力データ:辞書型(キー: クラス名、値: 生徒名と点数の辞書)で与えられる
- 出力:最終結果は文字列でコンソールに表示する
2. 登場するクラスを洗い出す
School:学校全体
ClassRoom:学校内のクラス単位の集まり
Student:個々の生徒
3. 各クラスの責務を決める
Student:生徒のデータ保持とデータが正しいか検証する(データの整合性はデータ自身に最も近い場所で保証するのがベスト!Schoolなどの上位クラスが全部検証すると、変更に対応するのが難しくなる)
属性:name, points
メソッド:生徒の情報を文字列で返却する
ClassRoom:複数のstudentのデータを保持し、クラス単位の集計を行う
属性:room_name, students
メソッド:生徒オブジェクトをリストに追加、クラス全員の合計点を返す、クラスの平均点を返す
School:入力データの解析、複数のクラスのデータを集約、学校全体の集計、データの出力
属性:ClassRoomオブジェクト
メソッド:入力データからStudentとClassRoomオブジェクトを作成する、全クラスのデータから合計点を計算して返却する、データをコンソールに表示する表示ロジックを分離する
4. クラス間の関係を整理する
ClassRoom は複数の Student オブジェクトを持つ ⇒コンポジション
School は複数の ClassRoom オブジェクトを持つ⇒コンポジション
5. getter/setterを使うデータを選ぶ(カプセル化)
points:点数が数値型で0~100の間の値であること
name:生徒の名前
完成コード
class Student:
def __init__(self, name, points):
self.name = name
self.points = points
@property
def name(self):
return self._name
@name.setter # 空の名前入力を防ぐ
def name(self, value):
if not value:
raise ValueError('名前は必須です')
self._name = value
@property
def points(self):
return self._points
@points.setter # 点数の不正な値の入力を防ぐ
def points(self, value):
if not isinstance(value, (int,float)):
raise TypeError('点数は数値で入力してください')
if not 0<= value <= 100:
raise ValueError('点数は0~100点で入力してください')
self._points = value
def __str__(self):
return f"{self.name} : {self.points}"
class ClassRoom:
def __init__(self, room_name):
self.room_name = room_name
self.students = []
# self.total_points = 0
def add_student(self, student):
self.students.append(student)
return self.students
def count_students(self):
return len(self.students)
def calculate_total_points(self):
return sum(student.points for student in self.students)
def calculate_avg(self):
if not self.students:
return 0
return self.calculate_total_points() / len(self.students)
class School:
def __init__(self, classes_data=None):
self.classrooms = []
if classes_data:
self.load_from_dict(classes_data)
def load_from_dict(self, classes_data):
"""外部データを取得するメソッド"""
for class_name, student_dict in classes_data.items():
class_room = ClassRoom(class_name)
for student_name, student_points in student_dict.items():
student = Student(student_name, student_points)
class_room.add_student(student)
self.add_classroom(class_room)
def add_classroom(self, classroom):
self.classrooms.append(classroom)
def calculate_total_avg(self):
total_point = 0
num_students = 0
for classroom in self.classrooms:
total_point += classroom.calculate_total_points()
num_students += classroom.count_students()
return total_point / num_students
def get_all_results(self):
"""データを取得・整理(表示に依存しない)"""
results = {
'classrooms': [],
'total_average': self.calculate_total_avg()
}
for classroom in self.classrooms:
classroom_data = {
'name': classroom.room_name,
'students': [(student.name, student.points) for student in classroom.students],
'average': classroom.calculate_avg()
}
results['classrooms'].append(classroom_data)
return results
def display_results(self):
"""表示専用メソッド"""
results = self.get_all_results()
for classroom_data in results['classrooms']:
print(f"{classroom_data['name']}の成績処理を開始します")
for name, points in classroom_data['students']:
print(f'{name} : {points}')
print(f"{classroom_data['name']}の平均点: {classroom_data['average']:.2f}点")
print(f"学校全体の平均点:{results['total_average']:.2f}")
classes = {
"1年A組": {
"佐藤": 85, "鈴木": 92, "高橋": 78
},
"1年B組": {
"田中": 88, "渡辺": 76, "山本": 90, "中村": 85
},
"1年C組": {
"伊藤": 80, "山田": 83
}
}
school_processor = School(classes)
result = school_processor.display_results()
改善点
クラスを利用したことで下記が改善されました~
- データの検証不足
setterでカプセル化してデータを保証 - ロジックの分離不足
計算、管理、表示を分けて責務を分離
表示方法を変更したい場合にもdisplay_resultsだけ修正すればよくなる
まとめ
関数ベースのコードでも要件を満たすことはできますが、将来的な変更や機能追加に耐えられるコードがクラス設計のポイントです
参考記事