Python / LESSON 14 / PY14

Python:継承と特殊メソッド

1学習の目的

2基礎解説

やりたいこと書き方意味
継承するclass Dog(Animal):親の属性・メソッドを引き継ぐ
親の初期化を呼ぶsuper().__init__(name)親の __init__ を実行する
上書きする親と同名のメソッドを定義オーバーライド
組み込み機能に対応__len__ / __eq__ / __add__len(x) / x == y / x + y
✅ 覚えるべき重要ポイント
  1. class 子(親): と書くだけで親の中身を全部引き継ぐ。共通部分は親、違う部分だけ子に書くのが設計の基本。
  2. 子で __init__ を定義したら必ず super().__init__(...) を呼ぶ。忘れると親が作るはずの属性が存在せず AttributeError になる。
  3. 親と同名のメソッドを子に書くと上書きされる。親の処理も使いたければ super().メソッド名() で呼び出せる。
  4. isinstance(obj, Animal) は「そのクラスまたはその子クラスのインスタンスか」を判定する。
  5. 特殊メソッドはPythonの標準構文と自作クラスをつなぐ橋__len__ を書けば len() が、__add__ を書けば + が使えるようになる。

現場使用例:一般会員クラスからプレミアム会員を派生、共通のAPIクライアントから各サービス用を派生、Exception を継承した自作エラークラス、図形や従業員など「同じ仲間だが計算方法が違う」もの。

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

    def __str__(self):
        return f"{self.name}({self.speak()})"


class Dog(Animal):
    def speak(self):          # 上書き
        return "ワン"


class Cat(Animal):
    def __init__(self, name, indoor):
        super().__init__(name)    # 親の初期化を呼ぶ
        self.indoor = indoor

    def speak(self):
        return "ニャー"


print(Dog("ポチ"))                        # ポチ(ワン)
print(Cat("タマ", True))                  # タマ(ニャー)
print(isinstance(Dog("ポチ"), Animal))    # True

3基本ドリル(10問)

PY14-D01 speak メソッドが "..." を返す Animal クラスを作り、それを継承した Dog クラスで speak を "ワン" に上書きせよ。Dog().speak() の結果を出力すること。 コーディング★☆☆無料
期待される結果

出力 == ワン

ヒント

class Dog(Animal): と書き、中で同名の speak を定義すれば上書きされる。

模範解答
class Animal:
    def speak(self):
        return "..."


class Dog(Animal):
    def speak(self):
        return "ワン"


print(Dog().speak())
解説

オーバーライドの最小形。親と同じ名前で定義するだけで、子のほうが優先される。

PY14-D02 Animal を継承した Dog クラスの正しい定義はどれか。
A. class Dog: Animal B. class Dog(Animal): C. class Dog extends Animal: D. class Dog -> Animal:
選択★☆☆無料
期待される結果

解答 == B

ヒント

Pythonはカッコで親を指定する。

模範解答
B
解説

extends は Java や JavaScript の書き方。Pythonはクラス名の後ろに丸カッコで親を書く。

PY14-D03 name 属性と introduce メソッド(「私は○○です」を返す)を持つ Animal クラスを作り、それを継承した Dog クラスを中身を追加せずに定義せよ。Dog("ポチ").introduce() を出力すること。 コーディング★☆☆無料
期待される結果

出力 == 私はポチです

ヒント

中身を書かないクラスは pass と1行書けばよい。__init__ も introduce も親から引き継がれる。

模範解答
class Animal:
    def __init__(self, name):
        self.name = name

    def introduce(self):
        return f"私は{self.name}です"


class Dog(Animal):
    pass


print(Dog("ポチ").introduce())
解説

何も書かなくても親の機能は全部使えるのが継承。まず親を継承だけしておき、必要になった部分から上書きしていく進め方ができる。

PY14-D04 子クラスから親の初期化を呼び出すコードを完成させよ。 穴埋め★☆☆無料
コード
class Animal:
    def __init__(self, name):
        self.name = name


class Cat(Animal):
    def __init__(self, name, indoor):
        ____.__init__(name)
        self.indoor = indoor


c = Cat("タマ", True)
print(c.name)
期待される結果

出力 == タマ

ヒント

「親(スーパークラス)」を表す組み込み関数。カッコを付けて呼ぶ。

模範解答
super()
解説

super() は親クラスを指す。ここで name を渡しているので、親の __init__ が self.name を作ってくれる。

PY14-D05 子クラスの __init__ を定義したのに super().__init__() を呼ばないとどうなるか。
A. 必ずエラーで止まる B. 親の __init__ が実行されず、親で作られるはずの属性が存在しない C. 自動的に呼ばれるので問題ない D. 子の __init__ が無視される
選択★☆☆無料
期待される結果

解答 == B

ヒント

定義した時点ではエラーにならない。問題が出るのはいつか。

模範解答
B
解説

定義時ではなく、その属性を使った瞬間に AttributeError が出る。原因が離れた場所で発覚するので厄介。子で __init__ を書いたら super() を呼ぶ、と機械的に覚えるとよい。

PY14-D06 商品リストを持つ Cart クラスに __len__ を定義し、len(cart) で件数が取れるようにせよ。["ノート", "ペン", "消しゴム"] を入れて len() の結果を出力すること。 コーディング★★☆無料
期待される結果

出力 == 3

ヒント

__len__ の中で return len(self.items) と書けばよい。

模範解答
class Cart:
    def __init__(self, items):
        self.items = items

    def __len__(self):
        return len(self.items)


cart = Cart(["ノート", "ペン", "消しゴム"])
print(len(cart))
解説

cart.count() のような独自メソッドを覚えてもらう必要がなくなるのが特殊メソッドの価値。使う人は普通に len() と書けばよい。

PY14-D07 座標が等しいかを == で比較できるようにするメソッドを完成させよ。 穴埋め★★☆無料
コード
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def ____(self, other):
        return self.x == other.x and self.y == other.y


print(Point(1, 2) == Point(1, 2))
期待される結果

出力 == True

ヒント

equal(等しい)の略。前後にアンダースコア2つ。

模範解答
__eq__
解説

定義しないと、中身が同じでも別のオブジェクトなので False になる。データの一致を判定したいクラスには必要。

PY14-D08 Dog が Animal を継承しているとき isinstance(Dog(), Animal) の結果はどれか。
A. True B. False C. エラーになる D. None
選択★★☆無料
期待される結果

解答 == A

ヒント

子クラスは親クラスの一種とみなされるか。

模範解答
A
解説

子は親の一種と判定される。だから「Animalのリスト」に Dog も Cat も混ぜて入れられる。

PY14-D09 割引率を返す discount メソッド(0を返す)を持つ Member クラスを作り、それを継承した Premium クラスで 親の値に10を足した値を返すよう上書きせよ。Premium().discount() を出力すること。 コーディング★★☆無料
期待される結果

出力 == 10

ヒント

子の discount の中で super().discount() を呼び、その結果に 10 を足す。

模範解答
class Member:
    def discount(self):
        return 0


class Premium(Member):
    def discount(self):
        return super().discount() + 10


print(Premium().discount())
解説

上書きしつつ親の処理も使う形。親のロジックが変わっても、子は自動的にその変更を引き継げる。

PY14-D10 金額を持つ Money クラスに __add__ を定義し、Money(1000) + Money(500) ができるようにせよ。足した結果の金額を出力すること。 コーディング★★☆無料
期待される結果

出力 == 1500

ヒント

__add__ は新しい Money を返すようにする。return Money(self.amount + other.amount)。

模範解答
class Money:
    def __init__(self, amount):
        self.amount = amount

    def __add__(self, other):
        return Money(self.amount + other.amount)


total = Money(1000) + Money(500)
print(total.amount)
解説

元のオブジェクトを書き換えず新しいものを返すのが + の作法。1000 + 500 が元の数値を壊さないのと同じ考え方。

4実践シナリオ(5問)

PY14-S01 【会員種別で割引を変える】Member クラス(種別「一般」・割引率0)を作り、Premium(「プレミアム」・0.1)と VIP(「VIP」・0.2)を継承で定義せよ。支払額を返す final_price(price) と __str__(「田中(一般)」形式)は親に持たせること。
田中(一般)・鈴木(プレミアム)・佐藤(VIP)の3人について、10,000円の商品の支払額を「田中(一般):10,000円」の形式で出力せよ。
コーディング★★☆無料
期待される結果

出力3行が完全一致

ヒント

子の __init__ で super().__init__(name, ...) を呼び、種別と割引率だけを変える。計算と表示は親に1回書けばよい。

模範解答
class Member:
    def __init__(self, name, kind="一般", rate=0):
        self.name = name
        self.kind = kind
        self.rate = rate

    def final_price(self, price):
        return int(price * (1 - self.rate))

    def __str__(self):
        return f"{self.name}({self.kind})"


class Premium(Member):
    def __init__(self, name):
        super().__init__(name, "プレミアム", 0.1)


class VIP(Member):
    def __init__(self, name):
        super().__init__(name, "VIP", 0.2)


members = [Member("田中"), Premium("鈴木"), VIP("佐藤")]

for m in members:
    print(f"{m}:{m.final_price(10000):,}円")
解説

計算式は親に1回だけ書けばよい。会員種別が10種類に増えても、子クラスは __init__ の1行ずつで済む。PY04で if/elif を並べた割引処理と見比べてほしい。

PY14-S02 【図形の面積】Shape クラス(name属性、area は0を返す、__str__ は「長方形:24.00」形式で小数2桁)を作り、Rectangle(w, h) と Circle(r) を継承で定義せよ(円周率は3.14)。Rectangle(4, 6) と Circle(5) を出力し、最後に「合計面積:102.50」と出力すること。 コーディング★★☆無料
期待される結果

出力3行が完全一致

ヒント

子の __init__ で super().__init__("長方形") のように名前を渡す。合計は sum(s.area() for s in shapes)。

模範解答
class Shape:
    def __init__(self, name):
        self.name = name

    def area(self):
        return 0

    def __str__(self):
        return f"{self.name}:{self.area():.2f}"


class Rectangle(Shape):
    def __init__(self, w, h):
        super().__init__("長方形")
        self.w = w
        self.h = h

    def area(self):
        return self.w * self.h


class Circle(Shape):
    def __init__(self, r):
        super().__init__("円")
        self.r = r

    def area(self):
        return self.r ** 2 * 3.14


shapes = [Rectangle(4, 6), Circle(5)]

for s in shapes:
    print(s)

print(f"合計面積:{sum(s.area() for s in shapes):.2f}")
解説

種類の違う図形を同じリストに入れて同じように扱えるのが継承の威力。print(s) と書くだけで、それぞれが自分の計算方法で面積を出す。これをポリモーフィズム(多態性)と呼ぶ。

PY14-S03 【自作の例外クラス】Exception を継承した StockError を定義せよ。商品名と在庫を持つ Product クラスに出荷メソッド ship(amount) を作り、在庫不足なら raise StockError(f"在庫が足りません(残り{在庫}個)") とすること。
在庫5の「ノート」に対して3個・5個の順に出荷を試み、「出荷しました:3個」「エラー:在庫が足りません(残り2個)」の2行を出力せよ。
コーディング★★☆無料
期待される結果

出力2行が完全一致

ヒント

class StockError(Exception): pass の2行で自作エラーが作れる。呼び出し側は except StockError as e: で受け止める。

模範解答
class StockError(Exception):
    pass


class Product:
    def __init__(self, name, stock):
        self.name = name
        self.stock = stock

    def ship(self, amount):
        if amount > self.stock:
            raise StockError(f"在庫が足りません(残り{self.stock}個)")
        self.stock -= amount
        return amount


p = Product("ノート", 5)

for amount in [3, 5]:
    try:
        print(f"出荷しました:{p.ship(amount)}個")
    except StockError as e:
        print(f"エラー:{e}")
解説

自作エラーは2行で作れる。ValueError で済ませず専用のエラーを作ると、呼び出し側が「在庫の問題だけ」を選んで捕まえられる。PY10の例外処理と継承がつながる場面。

PY14-S04 【特殊メソッドでカートを合体】商品(名前, 価格)のリストを持つ Cart クラスに、合計金額を返す total、__len__(点数)、__add__(2つのカートを合体した新しいカートを返す。名前は「合計」)、__str__(「カートA:2点 / 4,700円」形式)を定義せよ。
カートA[ノート1500, マウス3200] とカートB[キーボード3200] を作り、A・B・A+B の3行を出力すること。
コーディング★★★無料
期待される結果

出力3行が完全一致

ヒント

total は sum(price for _, price in self.items)。__add__ では self.items + other.items でリストを連結した新しい Cart を返す。__str__ の中では len(self) が使える。

模範解答
class Cart:
    def __init__(self, name, items):
        self.name = name
        self.items = items

    def total(self):
        return sum(price for _, price in self.items)

    def __len__(self):
        return len(self.items)

    def __add__(self, other):
        return Cart("合計", self.items + other.items)

    def __str__(self):
        return f"{self.name}:{len(self)}点 / {self.total():,}円"


a = Cart("カートA", [("ノート", 1500), ("マウス", 3200)])
b = Cart("カートB", [("キーボード", 3200)])

print(a)
print(b)
print(a + b)
解説

a + b と書けるようになったのが成果。使う人はカートの内部構造を知らなくても直感的に扱える。タプルの分解で名前が不要なときは _ という変数名を使う慣習も覚えておきたい。

PY14-S05 【従業員の給与計算】Employee クラス(name・雇用区分 kind、salary() は0、__str__ は「田中(正社員):300,000円」形式)を作り、月給制の SalariedEmployee(name, monthly) と時給制の HourlyEmployee(name, wage, hours) を継承で定義せよ。
田中(月給300,000)・鈴木(時給1,600円×100時間)・佐藤(月給350,000)の3人を出力し、最後に「合計人件費:810,000円」と出力すること。
コーディング★★★無料
期待される結果

出力4行が完全一致

ヒント

子の __init__ で super().__init__(name, "正社員") のように区分を渡す。salary() を子ごとに上書きし、合計は sum(s.salary() for s in staff)。

模範解答
class Employee:
    def __init__(self, name, kind):
        self.name = name
        self.kind = kind

    def salary(self):
        return 0

    def __str__(self):
        return f"{self.name}({self.kind}):{self.salary():,}円"


class SalariedEmployee(Employee):
    def __init__(self, name, monthly):
        super().__init__(name, "正社員")
        self.monthly = monthly

    def salary(self):
        return self.monthly


class HourlyEmployee(Employee):
    def __init__(self, name, wage, hours):
        super().__init__(name, "時給")
        self.wage = wage
        self.hours = hours

    def salary(self):
        return self.wage * self.hours


staff = [
    SalariedEmployee("田中", 300000),
    HourlyEmployee("鈴木", 1600, 100),
    SalariedEmployee("佐藤", 350000),
]

for s in staff:
    print(s)

print(f"合計人件費:{sum(s.salary() for s in staff):,}円")
解説

給与の計算方法が違っても、集計側は salary() を呼ぶだけ。歩合制や業務委託が増えても、集計コードは1文字も変えずに済む。これが「拡張に強い設計」の具体例。

5仕上げ課題

PY14-FINAL 【ECサイトの注文システム】継承・特殊メソッド・自作例外をすべて使って注文処理を実装せよ。

OutOfStockErrorException を継承した自作エラー

Product クラス(親):属性 name / price / stock
 ・label() … "商品" を返す ・needs_shipping() … False を返す
 ・take(qty) … 在庫不足なら raise OutOfStockError(f"「{name}」の在庫が足りません(残り{stock}個)")、足りていれば在庫を減らす

DigitalProduct:label は "電子"(送料は不要のまま)
PhysicalProduct:label は "物理"、needs_shipping は True

Order クラス:属性 store / lines(空リスト)
 ・add(product, qty) … take してから lines に (product, qty) を追加
 ・__len__ … 合計点数 ・subtotal() … 商品小計
 ・shipping() … 送料が必要な商品が1つでもあれば800、なければ0
 ・total() … 小計+送料

処理:ノートPC(128000, 在庫5)×1、マウス(3200, 在庫2)×2、Python入門eBook(2400, 在庫999)×1 を注文して明細と集計を表示 → 最後にマウスをもう1個追加しようとして失敗する。

出力仕様(11行):
=== ゼロカラストア 注文 ===
[物理] ノートPC:128,000円 × 1
[物理] マウス:3,200円 × 2
[電子] Python入門eBook:2,400円 × 1
---
商品点数:4点
商品小計:136,800円
送料:800円
合計:137,600円
---
エラー:「マウス」の在庫が足りません(残り0個)
期待される結果

出力11行が完全一致

ヒント

送料判定は for で lines を回し、needs_shipping() が True の商品が見つかった時点で 800 を return する。明細は for p, q in order.lines: でタプルを分解して取り出す。最後の追加は try / except OutOfStockError as e: で受け止める。

模範解答
class OutOfStockError(Exception):
    pass


class Product:
    def __init__(self, name, price, stock):
        self.name = name
        self.price = price
        self.stock = stock

    def label(self):
        return "商品"

    def needs_shipping(self):
        return False

    def take(self, qty):
        if qty > self.stock:
            raise OutOfStockError(f"「{self.name}」の在庫が足りません(残り{self.stock}個)")
        self.stock -= qty


class DigitalProduct(Product):
    def label(self):
        return "電子"


class PhysicalProduct(Product):
    def label(self):
        return "物理"

    def needs_shipping(self):
        return True


class Order:
    def __init__(self, store):
        self.store = store
        self.lines = []

    def add(self, product, qty):
        product.take(qty)
        self.lines.append((product, qty))

    def __len__(self):
        return sum(qty for _, qty in self.lines)

    def subtotal(self):
        return sum(p.price * q for p, q in self.lines)

    def shipping(self):
        for p, _ in self.lines:
            if p.needs_shipping():
                return 800
        return 0

    def total(self):
        return self.subtotal() + self.shipping()


notepc = PhysicalProduct("ノートPC", 128000, 5)
mouse = PhysicalProduct("マウス", 3200, 2)
ebook = DigitalProduct("Python入門eBook", 2400, 999)

order = Order("ゼロカラストア")
order.add(notepc, 1)
order.add(mouse, 2)
order.add(ebook, 1)

print(f"=== {order.store} 注文 ===")

for p, q in order.lines:
    print(f"[{p.label()}] {p.name}:{p.price:,}円 × {q}")

print("---")
print(f"商品点数:{len(order)}点")
print(f"商品小計:{order.subtotal():,}円")
print(f"送料:{order.shipping():,}円")
print(f"合計:{order.total():,}円")

print("---")

try:
    order.add(mouse, 1)
except OutOfStockError as e:
    print(f"エラー:{e}")
解説

電子書籍と物理商品という性質の違うものを、Order は区別せず同じように扱えているのが最大の到達点。Order のコードには「電子なら」「物理なら」という分岐が一切ない。送料の要否は商品自身が答えるので、将来「定期購読商品」が増えても Order は無修正で動く。継承(親子)・ポリモーフィズム(同じ呼び方で違う動作)・特殊メソッド(len)・自作例外——オブジェクト指向の主要な道具がすべて噛み合った形で、ここまで書ければ実務のシステム設計の入口に立っている。