Python:クラスとオブジェクト指向
1学習の目的
- class でデータ(属性)と処理(メソッド)を1つにまとめられるようになる。バラバラの変数と関数を管理する必要がなくなる。
- 同じ設計から複数のインスタンスを作り、それぞれが独立した状態を持つ仕組みを理解する。商品・ユーザー・注文といった「モノ」を素直にコードで表現できるようになる。
2基礎解説
| やりたいこと | 書き方 | 意味 |
|---|---|---|
| 設計図を作る | class Item: | クラス定義。名前は大文字始まり |
| 初期化する | def __init__(self, name): | インスタンス作成時に自動で呼ばれる |
| データを持つ | self.name = name | 属性。インスタンスごとに独立 |
| 実物を作る | item = Item("ノート") | インスタンス化。self は書かない |
- クラスは「設計図」、インスタンスは「実物」。1つの設計図から実物をいくつでも作れる。
- __init__ はインスタンスを作った瞬間に自動で呼ばれる。呼び出し時に self は書かない(Item("ノート"))。
- メソッドの第1引数は必ず self。self はその実物自身を指し、self.name でそのインスタンスのデータを読み書きする。
- インスタンスはそれぞれ独立。a のデータを変えても b には影響しない。ここがモジュールとの決定的な違い。
- __str__ を定義すると print(オブジェクト) の表示を自分で決められる。デバッグや一覧表示が一気に楽になる。
現場使用例:商品・ユーザー・注文などデータのまとまり、銀行口座のような状態を持つもの、APIクライアント、ゲームのキャラクター。「名詞」で表せるものはクラスの候補。
class Item:
def __init__(self, name, price, qty):
self.name = name
self.price = price
self.qty = qty
def subtotal(self):
return self.price * self.qty
def __str__(self):
return f"{self.name}:{self.subtotal():,}円"
item = Item("ノートPC", 128000, 2)
print(item.name) # ノートPC
print(item.subtotal()) # 256000
print(item) # ノートPC:256,000円
3基本ドリル(10問)
出力 == 田中
__init__ の中で self.name = name と代入する。取り出しは p.name。
class Person:
def __init__(self, name):
self.name = name
p = Person("田中")
print(p.name)
クラスの最小形。__init__ で受け取った値を self に入れておくと、あとから属性として取り出せる。
A. __main__ B. __init__ C. __str__ D. __name__ 選択★☆☆無料
解答 == B
「初期化」を意味する英単語の略。
B
init は initialize(初期化)の略。__str__ は print したときに呼ばれるもので、タイミングが違う。
出力 == 314.0
面積は 半径 ** 2 * 3.14。メソッドの中では self.r で半径を参照する。
class Circle:
def __init__(self, r):
self.r = r
def area(self):
return self.r ** 2 * 3.14
c = Circle(10)
print(c.area())
メソッドは引数を渡さなくても自分のデータを使えるのが利点。関数なら area(10) と半径を毎回渡す必要がある。
class Dog:
def __init__(____, name):
self.name = name
d = Dog("ポチ")
print(d.name)
出力 == ポチ
「その実物自身」を指す、決まった名前の引数。
self
self という名前は慣習だが、Pythonのほぼ全てのコードがこれに従っている。変えてはいけないと考えてよい。
A. クラス自体 B. そのインスタンス自身 C. 親クラス D. 何も指さない 選択★☆☆無料
解答 == B
設計図と実物、どちらを指しているか。
B
a.method() と書くと、Pythonが裏で a を self として渡している。だから呼び出し側では self を書かない。
出力 == 1500
deposit の中で self.balance += amount とすれば属性を書き換えられる。
class Account:
def __init__(self, balance):
self.balance = balance
def deposit(self, amount):
self.balance += amount
a = Account(1000)
a.deposit(500)
print(a.balance)
メソッドで自分の状態を変えるのがクラスの本領。関数だと変更後の値を return して受け取り直す必要がある。
class Item:
def __init__(self, name):
self.name = name
def ____(self):
return f"商品:{self.name}"
print(Item("ノート"))
出力 == 商品:ノート
文字列(string)に変換するときに呼ばれる特殊メソッド。前後にアンダースコア2つ。
__str__
定義しないと <__main__.Item object at 0x...> という読めない表示になる。一覧を出すクラスには必ず付けたい。
A. 一緒に増える B. 影響を受けない C. エラーになる D. 0に戻る 選択★★☆無料
解答 == B
同じ設計図から作った実物同士は、データを共有するのか。
B
インスタンスごとにデータは独立。だから商品を100個作っても、それぞれが自分の在庫数を持てる。
出力3行。ノート:150円 ほか
リストの中に Item(...) を並べて作り、for item in items: で回す。
class Item:
def __init__(self, name, price):
self.name = name
self.price = price
items = [Item("ノート", 150), Item("ペン", 100), Item("消しゴム", 80)]
for item in items:
print(f"{item.name}:{item.price}円")
オブジェクトのリストは実務で最もよく使う形。PY06のタプルのリストより、名前でアクセスできるぶん読みやすい。
出力 == 4500 / 4950(2行)
メソッドの中から別のメソッドを呼ぶときも self.subtotal() と書く。
class Item:
def __init__(self, price, qty):
self.price = price
self.qty = qty
def subtotal(self):
return self.price * self.qty
def with_tax(self):
return int(self.subtotal() * 1.1)
item = Item(1500, 3)
print(item.subtotal())
print(item.with_tax())
メソッド同士を組み合わせられる。計算の重複をクラスの中で解消できるので、小計の求め方が変わっても1箇所直せば済む。
4実践シナリオ(5問)
出力4行が完全一致
__str__ の中で self.subtotal() を呼べる。合計は sum(i.subtotal() for i in items) で求められる。
class Item:
def __init__(self, name, price, qty):
self.name = name
self.price = price
self.qty = qty
def subtotal(self):
return self.price * self.qty
def __str__(self):
return f"{self.name}:{self.price:,}円 × {self.qty} = {self.subtotal():,}円"
items = [
Item("ノートPC", 128000, 1),
Item("マウス", 3200, 2),
Item("USBメモリ", 1500, 3),
]
for item in items:
print(item)
print(f"合計:{sum(i.subtotal() for i in items):,}円")
PY08・PY12と同じ請求書だが、表示形式がクラスの中に閉じ込められた。明細の見た目を変えたければ __str__ だけを直せばよい。
出力2行が完全一致
judge の中で self.average() を呼んで判定する。平均は {x:.1f} で小数1桁表示。
class Student:
def __init__(self, name, scores):
self.name = name
self.scores = scores
def average(self):
return sum(self.scores) / len(self.scores)
def judge(self):
if self.average() >= 60:
return "合格"
return "不合格"
students = [
Student("田中", [82, 91, 67, 76]),
Student("鈴木", [45, 60, 38, 67]),
]
for s in students:
print(f"{s.name}:平均{s.average():.1f}点 → {s.judge()}")
属性にリストを持たせられるのがポイント。名前と点数が1つのオブジェクトにまとまるので、PY06の並行リストのような順序ズレは原理的に起きない。
出力4行が完全一致
失敗する出金は try / except ValueError as e: で受け止める。PY10の例外処理をそのまま使う。
class Account:
def __init__(self, balance=0):
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("残高が不足しています")
self.balance -= amount
account = Account(10000)
account.deposit(5000)
print(f"入金後:{account.balance:,}円")
account.withdraw(3000)
print(f"出金後:{account.balance:,}円")
try:
account.withdraw(999999)
except ValueError as e:
print(f"エラー:{e}")
print(f"最終残高:{account.balance:,}円")
不正な操作をクラス自身が拒否する設計。残高チェックを使う側に任せると書き忘れが起きるが、メソッドの中で守れば絶対に破られない。失敗しても残高が変わっていない点も重要。
出力2行が完全一致
__init__ の中で self.count = 0 と初期化する。a.reset() を呼んでも b は変わらない。
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
def reset(self):
self.count = 0
a = Counter()
b = Counter()
a.increment()
a.increment()
a.increment()
b.increment()
print(f"a: {a.count} / b: {b.count}")
a.reset()
print(f"リセット後 a: {a.count} / b: {b.count}")
b が影響を受けないことがこの問題の答え。同じクラスから作っても実物は別物。PY12のモジュールは全体で1つしか存在しないので、ここが両者の使い分けの分かれ目になる。
ボールペン120・ノート8・ファイル3 の3件を出力し、「--- 補充後 ---」を挟んで、要発注の商品だけ50個補充して「ノート:58個」の形式で出力すること。 コーディング★★★無料
出力6行が完全一致
__str__ の中で三項演算子を使い、is_low() の結果で記号を切り替える。補充ループでは if p.is_low(): で絞る。
class Product:
def __init__(self, name, stock, threshold=10):
self.name = name
self.stock = stock
self.threshold = threshold
def is_low(self):
return self.stock < self.threshold
def restock(self, amount):
self.stock += amount
def __str__(self):
mark = " ⚠発注" if self.is_low() else ""
return f"{self.name}:{self.stock}個{mark}"
products = [
Product("ボールペン", 120),
Product("ノート", 8),
Product("ファイル", 3),
]
for p in products:
print(p)
print("--- 補充後 ---")
for p in products:
if p.is_low():
p.restock(50)
print(f"{p.name}:{p.stock}個")
PY06・PY07・PY09で何度も書いた在庫チェックの最終形。判定ロジックも表示形式も商品自身が持っているので、使う側は p.is_low() と print(p) を呼ぶだけでよくなった。
5仕上げ課題
Book クラス:属性 title / author / is_borrowed(初期値 False)
・borrow() … 貸出中なら raise ValueError("「タイトル」は貸出中です")、そうでなければ貸出中にする
・give_back() … 返却状態に戻す
・__str__ … 「[貸出可] Python入門 / 山田太郎」の形式(貸出中なら [貸出中])
Library クラス:属性 name / books(空リストで開始)
・add(book) … 蔵書に追加
・find(title) … タイトルで探して Book を返す(無ければ None)
・available_count() … 貸出可能な冊数を返す
処理:「ゼロカラ図書館」に3冊を追加して一覧表示 → ["Python入門", "Web開発入門", "Python入門"] の順に貸出(3回目は失敗する)→ 再度一覧表示と集計。
出力仕様(13行):
=== ゼロカラ図書館 ===
[貸出可] Python入門 / 山田太郎
[貸出可] データ分析の基礎 / 佐藤花子
[貸出可] Web開発入門 / 鈴木一郎
---
「Python入門」を貸し出しました
「Web開発入門」を貸し出しました
エラー:「Python入門」は貸出中です
---
[貸出中] Python入門 / 山田太郎
[貸出可] データ分析の基礎 / 佐藤花子
[貸出中] Web開発入門 / 鈴木一郎
貸出可能:1冊 / 全3冊
出力13行が完全一致
Library の books は __init__ の中で self.books = [] と初期化する。find は for で回して title が一致した Book を return、見つからなければ最後に return None。available_count は内包表記で [b for b in self.books if not b.is_borrowed] の件数を数えるとよい。貸出は try / except ValueError as e: で受け止める。
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
self.is_borrowed = False
def borrow(self):
if self.is_borrowed:
raise ValueError(f"「{self.title}」は貸出中です")
self.is_borrowed = True
def give_back(self):
self.is_borrowed = False
def __str__(self):
status = "貸出中" if self.is_borrowed else "貸出可"
return f"[{status}] {self.title} / {self.author}"
class Library:
def __init__(self, name):
self.name = name
self.books = []
def add(self, book):
self.books.append(book)
def find(self, title):
for book in self.books:
if book.title == title:
return book
return None
def available_count(self):
return len([b for b in self.books if not b.is_borrowed])
library = Library("ゼロカラ図書館")
library.add(Book("Python入門", "山田太郎"))
library.add(Book("データ分析の基礎", "佐藤花子"))
library.add(Book("Web開発入門", "鈴木一郎"))
print(f"=== {library.name} ===")
for book in library.books:
print(book)
print("---")
for title in ["Python入門", "Web開発入門", "Python入門"]:
book = library.find(title)
try:
book.borrow()
print(f"「{title}」を貸し出しました")
except ValueError as e:
print(f"エラー:{e}")
print("---")
for book in library.books:
print(book)
print(f"貸出可能:{library.available_count()}冊 / 全{len(library.books)}冊")
クラスがクラスを持つ(Library が Book のリストを持つ)構造が書けたことが最大の到達点。実務のシステムはほぼこの形で、ユーザーが注文を持ち、注文が商品を持つ、といった入れ子で組み立てられている。貸出可否の判定は Book 自身が、蔵書の管理は Library が担当する——それぞれのクラスが自分の責任範囲だけを知っている状態が、オブジェクト指向設計の目指すゴール。