🐍

精読「独習Python」(オブジェクト指向構文)

2025/01/01に公開



独習Python
「独習Python」は、初心者から中級者までを対象に、Pythonの基礎から応用までを体系的に学べる入門書で、豊富な例題と練習問題を通じて実践的なスキルを身につけられる一冊です。

関連記事

クラスの定義

オブジェクト指向プログラミングで中心となるのはクラス

最も簡単なクラス

class MyClass:
    pass

インスタンスを作成

obj = MyClass()
print(obj)  # <__main__.MyClass object at 0x...>

インスタンス変数

インスタンス変数は、クラスから生成されたインスタンスごとに独立して保持されるデータを指す。

特徴

  • インスタンスごとに独立して存在
  • 異なるインスタンスはそれぞれ独自のインスタンス変数を持つ。
  • selfを通じて定義・参照される

インスタンス変数は、クラス内のメソッドでself.<変数名>という形で設定・アクセスする。

class Person:
    def __init__(self, name, age):
        self.name = name  # インスタンス変数 name を定義
        self.age = age    # インスタンス変数 age を定義

# インスタンスを生成
person1 = Person("Alice", 25)
person2 = Person("Bob", 30)

# インスタンス変数にアクセス
print(person1.name)  # "Alice"
print(person2.age)   # 30

他の変数との比較

種類 定義場所 スコープ
インスタンス変数 self.<変数名> インスタンスごとに独立して存在
クラス変数 クラスの中 (メソッド外) すべてのインスタンス間で共有されるクラス全体の変数
ローカル変数 メソッド内 メソッドが実行されている間だけ有効

また、「スロット(__slots__」は、クラスのインスタンスに動的に属性を追加するのを制限するための機能で、これを使用することで、インスタンスの属性に対して予め決められた名前しか使えなくなり、メモリの節約やパフォーマンスの向上を図ることができる。

class MyClass:
    __slots__ = ['name', 'age']

# インスタンスを作成
obj = MyClass()

# スロットで定義された属性にはアクセス可能
obj.name = 'Alice'
obj.age = 30

print(obj.name)  # Alice
print(obj.age)   # 30

# スロットに定義されていない属性にはアクセスできない
obj.address = '123 Main St'  # AttributeError: 'MyClass' object has no attribute 'address'

メソッド

メソッド(method) は、クラス内で定義された関数のこと。クラスに属するため、そのクラスのインスタンスやクラス自体を操作したり、データを処理したりするために使用される。メソッドは、関数と似ているが、主にオブジェクト指向プログラミング(OOP)の中でインスタンスやクラスに関連付けられたもの

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        return f"Hello, my name is {self.name} and I am {self.age} years old."

p = Person("Alice", 30)
print(p.greet())  # Hello, my name is Alice and I am 30 years old.

クラスメソッド

クラス自体に関連するメソッドで、インスタンスではなくクラスそのものに対して呼び出される。通常、クラスメソッドはクラスを操作するために使用され、インスタンスメソッドと異なり、インスタンスを必要としない。

クラスメソッドを定義するためには、@classmethod デコレータを使用する。クラスメソッドは、最初の引数としてクラス自体(通常 cls)を受け取る。これにより、クラス内の変数や他のメソッドにアクセスできる。

class MyClass:
    class_variable = "I am a class variable"

    @classmethod
    def print_class_variable(cls):
        print(cls.class_variable)

# クラスメソッドを呼び出し
MyClass.print_class_variable() # 出力:I am a class variable

クラスメソッドの主な用途は、インスタンスを作成するためのファクトリメソッドとして使うことが多い

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_string(cls, person_str):
        name, age = person_str.split(',')
        return cls(name, int(age))

# クラスメソッドを使用してインスタンスを作成
person = Person.from_string("Alice,30")
print(person.name, person.age)

staticメソッド

クラスに関連したメソッドですが、インスタンスやクラスそのものを引数として受け取らない点が特徴。static methodは、インスタンスメソッドやクラスメソッドとは異なり、特にクラスやインスタンスの状態に依存しない。そのため、クラスの外部からも呼び出すことができる。

静的メソッドを定義するには、@staticmethodデコレータを使用します。静的メソッドは、クラスやインスタンスを引数として取らず、通常の関数のように振る舞う。

class MyClass:
    @staticmethod
    def greet(name):
        print(f"Hello, {name}!")

# クラスインスタンスを作成せずに、クラスから直接呼び出すことができます
MyClass.greet("Alice")

クラス変数

クラス自体に関連付けられている変数であり、クラスのすべてのインスタンスで共有される。これにより、クラス変数はインスタンスごとに異なる値を持つことなく、すべてのインスタンスが同じ値を参照する。

クラス変数は、クラス定義内で通常の変数として宣言され、インスタンスを作成せずに直接クラスからアクセスすることもできる。また、インスタンスからもアクセス可能ですが、インスタンスごとにクラス変数が共有されるため、インスタンスからアクセスする場合、変更がクラス変数にも影響を与える点に注意が必要。

class MyClass:
    class_variable = "Initial value"

# インスタンスを作成
obj1 = MyClass("Alice")
obj2 = MyClass("Bob")

# クラス変数の変更
MyClass.class_variable = "Changed value"

print(obj1.class_variable)  # "Changed value"
print(obj2.class_variable)  # "Changed value"

カプセル化

オブジェクト指向プログラミング(OOP)の中核を成すのは、カプセル化継承ポリモーフィズムの3つの概念。これらはOOPの基礎を理解するために重要な考え方を含んでおり、これらを理解することで、よりオブジェクト指向らしいコードが書けるようになる。構文の理解だけでなく、その背後にある必要性や前提も学びながら進めることが大切。

カプセル化とは?

カプセル化は、「使い手に関係ないものは見せない」ことで、クラス内部の複雑な機能を隠し、必要な機能だけを公開する考え方。これにより、利用者は簡単に、安全に機能を使えるようになる。

インスタンス変数の隠蔽

インスタンス変数の隠蔽は、カプセル化の一部であり、クラス内部で使用される変数を外部からアクセスできないように隠すこと。これにより、クラスの使用者が不必要に内部状態を変更することを防ぎ、データの整合性を保つことができる。

Pythonでは、インスタンス変数を隠蔽するために、変数名の前にアンダースコア (_) をつけることがある(厳密な隠蔽ではなく、慣習的な方法)。さらに、__ を使うことで名前修飾(名前マングリング)を行い、より強力に隠蔽することもできる。

class MyClass:
    def __init__(self, value):
        self.__hidden_value = value  # 隠蔽されたインスタンス変数

    def get_value(self):
        return self.__hidden_value  # 外部からアクセス可能なメソッドを通じてアクセス

    def set_value(self, value):
        if value >= 0:
            self.__hidden_value = value  # 値を変更するメソッド

# インスタンスを作成
obj = MyClass(10)

# 直接アクセスしようとするとエラー
# print(obj.__hidden_value)  # AttributeError: 'MyClass' object has no attribute '__hidden_value'

# メソッドを通じてアクセス
print(obj.get_value())  # 10

# メソッドを通じて変更
obj.set_value(20)
print(obj.get_value())  # 20

アクセサーメソッド

インスタンス変数の隠蔽だけでは、クラス外から値が見えなくなり不便。そのため、アクセサーメソッド(ゲッター/セッター) を使用して、隠蔽されたインスタンス変数にアクセスできる仕組みを提供する。これにより、値の取得や設定時に任意の処理(例外処理や値の加工)を加えることができる。ゲッターを使えば読み取り専用に、セッターを使えば書き込み専用にすることも可能。状態が変更されないほうが管理しやすいため、必要ない場合はセッターを作らない方が良い。

class Person:
    def __init__(self, name, age):
        self.__name = name  # 隠蔽されたインスタンス変数
        self.__age = age    # 隠蔽されたインスタンス変数

    # ゲッター(取得用)メソッド
    def get_name(self):
        return self.__name

    def get_age(self):
        return self.__age

    # セッター(設定用)メソッド
    def set_name(self, name):
        self.__name = name

    def set_age(self, age):
        if age >= 0:  # 年齢が0以上かチェック
            self.__age = age
        else:
            print("年齢は0以上でなければなりません")

# インスタンスの作成
person = Person("Alice", 30)

# ゲッターを使って値を取得
print(person.get_name())  # Alice
print(person.get_age())   # 30

# セッターを使って値を変更
person.set_name("Bob")
person.set_age(25)

# 更新後の値を確認
print(person.get_name())  # Bob
print(person.get_age())   # 25

# 無効な値を設定しようとするとエラー
person.set_age(-5)  # 年齢は0以上でなければなりません

プロパティ

プロパティ(Property) は、クラスのインスタンス変数に対するアクセスを簡潔に制御できる仕組み。プロパティを使うことで、ゲッターやセッターをメソッドとして定義する代わりに、変数のようにアクセスできるようになる。

Pythonでは、property() 関数または @property デコレーターを使用して、インスタンス変数へのアクセスをカスタマイズすることができる。プロパティを使用すると、ゲッターとセッターのようなロジックをクラスの属性に結びつけて、より簡潔で直感的なコードを書くことができる。

class Person:
    def __init__(self, name, age):
        self.__name = name  # 隠蔽されたインスタンス変数
        self.__age = age    # 隠蔽されたインスタンス変数

    # プロパティを使ってゲッターとセッターを定義
    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, value):
        self.__name = value

    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, value):
        if value >= 0:
            self.__age = value
        else:
            print("年齢は0以上でなければなりません")

# インスタンスの作成
person = Person("Alice", 30)

# プロパティを使って値を取得
print(person.name)  # Alice
print(person.age)   # 30

# プロパティを使って値を設定
person.name = "Bob"
person.age = 25

# 更新後の値を確認
print(person.name)  # Bob
print(person.age)   # 25

# 無効な値を設定しようとするとエラー
person.age = -5  # 年齢は0以上でなければなりません

継承

既存のクラス(基底クラス)の機能を引き継ぎ、新たな機能を追加したり、元の機能を上書きする仕組み。これにより、コードの重複を避け、変更があった場合にも基底クラスを修正するだけで派生クラスにも変更が反映される。継承を使うことで、共通部分を基底クラスにまとめ、差分だけを派生クラスで定義することができ、効率的なコード管理が可能になる。

継承の基本

継承(Inheritance)の基本は、あるクラスが他のクラス(基底クラス)の機能を引き継ぎ、新たな機能を追加または上書きすること。

以下のポイントが重要

  • 命名規則
    派生クラスは基底クラスより具体的な名前を付け、継承関係が分かりやすくするために基底クラス名を末尾に付けることが推奨される

  • 多重継承
    Pythonでは、クラスは複数の基底クラスを持つことができ(多重継承)、これは他の言語にはない特徴

  • 派生クラスの拡張
    派生クラスは基底クラスの機能を引き継ぎつつ、新たなメソッドを追加することができる

  • is-aの関係: 継承を使う場合、基底クラスと派生クラスの関係が「is-a」(派生クラスは基底クラスの一種)であることが重要です。例えば、「BusinessPersonはPersonである」という関係

注意点としては、継承関係が不自然な場合(例えば、意味が通じない場合)、継承を避け、他の方法(コンポジションなど)を検討するべき

メソッドのオーバーライド

メソッドのオーバーライド(Method Overriding) とは、派生クラスで基底クラスから継承したメソッドを再定義(上書き)すること。これにより、基底クラスのメソッドを変更せずに、派生クラスで異なる挙動を実装できる。

class Animal:
    def speak(self):
        print("Animal speaks")

class Dog(Animal):
    def speak(self):  # Animalクラスのspeakメソッドをオーバーライド
        print("Dog barks")

# インスタンスを作成
animal = Animal()
dog = Dog()

# オーバーライドされたメソッドが呼ばれる
animal.speak()  # 出力: Animal speaks
dog.speak()     # 出力: Dog barks

superによる基底クラスの参照

super() は、基底クラス(親クラス)にアクセスするための関数で、派生クラス(子クラス)内から基底クラスのメソッドやプロパティにアクセスする際に使用する。super() を使うことで、直接基底クラスを参照することなく、親クラスの機能を呼び出すことができる。

class Animal:
    def speak(self):
        print("Animal speaks")

class Dog(Animal):
    def speak(self):
        super().speak()  # 基底クラス Animal の speak メソッドを呼び出す
        print("Dog barks")

dog = Dog()
dog.speak()
# 出力:
# Animal speaks
# Dog barks

多重継承とメソッドの検索順序

多重継承は、1つのクラスが複数の親クラスを継承する仕組み。Pythonでは、メソッド解決順序(MRO) に基づいて、メソッドを検索する。MROはC3線形化アルゴリズムにより決定され、継承の順番に従ってメソッドが検索される。

class A:
    def greet(self):
        print("Hello from A")

class B(A):
    def greet(self):
        super().greet()
        print("Hello from B")

class C(A):
    def greet(self):
        super().greet()
        print("Hello from C")

class D(B, C):
    def greet(self):
        super().greet()
        print("Hello from D")

d = D()
d.greet()

委譲

継承はコード再利用の一手段ですが、常に最適な方法ではない。特に、基底クラスと派生クラスが密接に依存しており、基底クラスの変更が派生クラスに影響を及ぼすため、継承の修正コストは高くなることがある。継承は、基底クラスと派生クラスが「is-a」の関係にある場合に適している

一方、リスコフの置換原則[1]に違反する継承は避けるべき。例えば、PenguinクラスのインスタンスをBirdクラスのインスタンスとして使うと、flyメソッドを呼び出したときに例外が発生する。これにより、Birdクラスが期待する動作(飛べる)をPenguinクラスが満たさないため、リスコフの置換原則に違反している。

class Bird:
    def fly(self):
        print("I can fly!")

class Penguin(Bird):
    def fly(self):
        raise Exception("I cannot fly!")

このような問題を解決するのが委譲。委譲では、クラスが別のオブジェクトをインスタンス変数として保持し、そのオブジェクトに処理を委ねることで、クラス同士の依存を緩め、より柔軟な設計が可能になる。委譲を使用すれば、インスタンス単位で委譲先を変更したり、複数のクラスに処理を委譲することもできる。

class FlyingBehavior:
    def fly(self):
        print("I can fly!")

class NoFlyingBehavior:
    def fly(self):
        print("I cannot fly!")

class Bird:
    def __init__(self, flying_behavior):
        # 委譲先となるオブジェクトをインスタンス変数として保持
        self.flying_behavior = flying_behavior

    def perform_fly(self):
        self.flying_behavior.fly()

class Penguin:
    def __init__(self):
        # Penguinは飛べないのでNoFlyingBehaviorを使用
        self.flying_behavior = NoFlyingBehavior()

    def perform_fly(self):
        self.flying_behavior.fly()

class Sparrow:
    def __init__(self):
        # Sparrowは飛べるのでFlyingBehaviorを使用
        self.flying_behavior = FlyingBehavior()

    def perform_fly(self):
        self.flying_behavior.fly()

# 使用例:
penguin = Penguin()
penguin.perform_fly()  # 結果:I cannot fly!

sparrow = Sparrow()
sparrow.perform_fly()  # 結果:I can fly!
  • FlyingBehaviorNoFlyingBehavior クラスで「飛べる」「飛べない」動作を定義。
  • Bird クラスは flying_behavior を保持し、perform_fly() メソッドで処理を委譲。
  • Penguin は NoFlyingBehavior を、Sparrow は FlyingBehavior をインスタンスとして保持。

結論として、クラス設計時には、継承よりも委譲を選択する方が適切な場合が多いとされている。

ミックスイン

ミックスイン (Mixin) とは、再利用可能な機能を持ったクラスで、他のクラスに機能を追加するためだけに継承されるもの。単独では動作せず、他のクラスに組み込むことで機能を提供する。ミックスインを利用することで、複数のクラスに機能を追加でき、柔軟に機能を拡張できる。

# ミックスインの定義
class LogMixin:
    def show_attr(self):
        # インスタンスの属性を列挙
        return ', '.join(f"{key}: {value}" for key, value in self.__dict__.items())

# 基本クラスの定義
class Person(LogMixin):  # PersonクラスがLogMixinを継承
    def __init__(self, name, age):
        self.name = name
        self.age = age

# 使用例
person = Person("鈴木修", 50)
print(person.show_attr())  # 結果: name: 鈴木修, age: 50

ポリモーフィズム

ポリモーフィズム多態性と訳されるが、つまりは「同じ名前のメソッドで異なる挙動を実現する」こと

ポリモーフィズムの基本

例えば、以下の例で同じメソッド speak を使って、異なるクラスが異なる動作をすることを示している。これがポリモーフィズム(多態性)。

class Animal:
    def speak(self):
        return "動物の音"

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

class Cat(Animal):
    def speak(self):
        return "ニャー"

# 使用例
animals = [Dog(), Cat()]

for animal in animals:
    print(animal.speak())

抽象メソッド

抽象メソッドは、クラス内で定義されるが、実装を持たないメソッド。その目的は、派生クラスが必ずそのメソッドをオーバーライドし、実装を提供することを強制することです。

特徴

  • 実装なし
    抽象メソッド自体は実装を持たず、pass キーワードや空のメソッド定義が置かれる
  • 派生クラスでのオーバーライド義務
  • 抽象メソッドを含むクラス(抽象クラス)を継承するすべての派生クラスは、その抽象メソッドを実装しなければならない。実装しなければ、その派生クラスも抽象クラスとして扱われ、インスタンス化できない。

定義方法

  • abcモジュール を使用し、ABC クラスを継承したクラスで定義する。
  • 抽象メソッドには、@abstractmethod デコレーターを使って、メソッドを抽象メソッドとして宣言する。
from abc import ABC, abstractmethod

# 抽象クラス Shape
class Shape(ABC):
    @abstractmethod
    def draw(self):
        pass  # 実装なし

# Circleクラス(Shapeを継承)
class Circle(Shape):
    def draw(self):
        print("円を描いています")

# Squareクラス(Shapeを継承)
class Square(Shape):
    def draw(self):
        print("四角形を描いています")

# インスタンス化
circle = Circle()
square = Square()

# メソッドを呼び出す
circle.draw()  # 結果: 円を描いています
square.draw()  # 結果: 四角形を描いています

isinstance関数

isinstance 関数は、指定したオブジェクトが特定のクラス(またはそのサブクラス)のインスタンスかどうかを判定するための関数。これにより、オブジェクトがある型を持っているかどうかを簡単にチェックできる。

class Animal:
    pass

class Dog(Animal):
    pass

a = Animal()
d = Dog()

print(isinstance(a, Animal))  # True
print(isinstance(d, Dog))     # True
print(isinstance(d, Animal))  # True (DogはAnimalのサブクラスだから)
print(isinstance(a, Dog))     # False

参考


脚注
  1. 派生クラスは基底クラスのインスタンスと完全に置き換え可能でなければならない ↩︎

Discussion