はじめに
この実験では、Python における継承の動作について学びます。具体的には、super()関数がどのように動作するか、および協調的な継承がどのように実装されるかに焦点を当てます。継承はオブジェクト指向プログラミングにおける基本的な概念であり、クラスが親クラスから属性とメソッドを継承してコードの再利用や階層的なクラス構造を実現することができます。
このハンズオン体験では、Python における単一継承や多重継承などのさまざまな種類の継承を理解します。また、super()関数を使用して継承階層をナビゲートし、協調的な多重継承の実用的な例を実装し、これらの概念を適用して検証システムを構築する方法を学びます。この実験中に作成される主要なファイルはvalidate.pyです。
単一継承と多重継承の理解
このステップでは、Python における 2 つの主要な継承の種類、単一継承と多重継承について学びます。継承はオブジェクト指向プログラミングにおける基本的な概念で、クラスが他のクラスから属性とメソッドを継承することを可能にします。また、複数の候補がある場合に Python がどのメソッドを呼び出すかを決定する方法、つまりメソッド解決(method resolution)についても見ていきます。
単一継承
単一継承とは、クラスが単一の祖先の列を形成する場合です。各クラスが 1 つの直接の親を持つ家族の木のようなものだと考えてください。それがどのように機能するかを理解するために、例を作成しましょう。
まず、WebIDE で新しいターミナルを開きます。ターミナルが開いたら、以下のコマンドを入力して Enter キーを押すことで Python インタープリタを起動します。
python3
Python インタープリタに入ったら、単一の継承チェーンを形成する 3 つのクラスを作成します。以下のコードを入力してください。
class A:
def spam(self):
print('A.spam')
class B(A):
def spam(self):
print('B.spam')
super().spam()
class C(B):
def spam(self):
print('C.spam')
super().spam()
このコードでは、クラスBはクラスAから継承し、クラスCはクラスBから継承しています。super()関数は親クラスのメソッドを呼び出すために使用されています。
これらのクラスを定義した後、Python がメソッドを検索する順序を調べることができます。この順序はメソッド解決順序(Method Resolution Order, MRO)と呼ばれます。クラスCの MRO を見るには、以下のコードを入力します。
C.__mro__
以下のような出力が表示されるはずです。
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
この出力は、Python がまずクラスC、次にクラスB、次にクラスA、最後に基底のobjectクラスでメソッドを検索することを示しています。
では、クラスCのインスタンスを作成し、そのspam()メソッドを呼び出しましょう。以下のコードを入力します。
c = C()
c.spam()
以下のような出力が表示されるはずです。
C.spam
B.spam
A.spam
この出力は、単一の継承チェーンにおいてsuper()がどのように機能するかを示しています。C.spam()がsuper().spam()を呼び出すと、B.spam()が呼び出されます。次に、B.spam()がsuper().spam()を呼び出すと、A.spam()が呼び出されます。
多重継承
多重継承は、クラスが複数の親クラスから継承することを可能にします。これにより、クラスはすべての親クラスの属性とメソッドにアクセスすることができます。この場合のメソッド解決がどのように機能するかを見てみましょう。
Python インタープリタに以下のコードを入力します。
class Base:
def spam(self):
print('Base.spam')
class X(Base):
def spam(self):
print('X.spam')
super().spam()
class Y(Base):
def spam(self):
print('Y.spam')
super().spam()
class Z(Base):
def spam(self):
print('Z.spam')
super().spam()
次に、複数の親クラスX、Y、Zから継承するクラスMを作成します。以下のコードを入力します。
class M(X, Y, Z):
pass
M.__mro__
以下のような出力が表示されるはずです。
(<class '__main__.M'>, <class '__main__.X'>, <class '__main__.Y'>, <class '__main__.Z'>, <class '__main__.Base'>, <class 'object'>)
この出力は、クラスMのメソッド解決順序を示しています。Python はこの順序でメソッドを検索します。
クラスMのインスタンスを作成し、そのspam()メソッドを呼び出しましょう。
m = M()
m.spam()
以下のような出力が表示されるはずです。
X.spam
Y.spam
Z.spam
Base.spam
super()は直近の親クラスのメソッドを呼び出すだけではなく、子クラスによって定義されたメソッド解決順序(MRO)に従うことに注意してください。
親クラスの順序が異なる別のクラスNを作成しましょう。
class N(Z, Y, X):
pass
N.__mro__
以下のような出力が表示されるはずです。
(<class '__main__.N'>, <class '__main__.Z'>, <class '__main__.Y'>, <class '__main__.X'>, <class '__main__.Base'>, <class 'object'>)
では、クラスNのインスタンスを作成し、そのspam()メソッドを呼び出しましょう。
n = N()
n.spam()
以下のような出力が表示されるはずです。
Z.spam
Y.spam
X.spam
Base.spam
これは重要な概念を示しています。Python の多重継承では、クラス定義における親クラスの順序がメソッド解決順序を決定します。super()関数は、どのクラスから呼び出された場合でもこの順序に従います。
これらの概念を探索し終えたら、以下のコードを入力して Python インタープリタを終了することができます。
exit()
継承を用いた検証システムの構築
このステップでは、継承を使って実用的な検証システムを構築します。継承はプログラミングにおける強力な概念で、既存のクラスを基に新しいクラスを作成することができます。これにより、コードを再利用し、より組織的でモジュール化されたプログラムを作成することができます。この検証システムを構築することで、継承を使ってさまざまな方法で組み合わせることができる再利用可能なコードコンポーネントを作成する方法を学びます。
基本検証クラスの作成
まず、検証器の基本クラスを作成する必要があります。これを行うには、WebIDE で新しいファイルを作成します。以下のように操作します。「File」>「New File」をクリックするか、キーボードショートカットを使用します。新しいファイルが開いたら、validate.pyと名付けます。
では、このファイルにコードを追加して、基本のValidatorクラスを作成しましょう。このクラスは、他のすべての検証器の基礎となります。
## validate.py
class Validator:
@classmethod
def check(cls, value):
return value
このコードでは、checkメソッドを持つValidatorクラスを定義しています。checkメソッドは値を引数として受け取り、その値をそのまま返します。@classmethodデコレータは、このメソッドをクラスメソッドにするために使用されています。これは、クラスのインスタンスを作成することなく、クラス自体でこのメソッドを呼び出すことができることを意味します。
型検証器の追加
次に、値の型をチェックする検証器を追加します。これらの検証器は、先ほど作成したValidatorクラスを継承します。validate.pyファイルに戻り、以下のコードを追加します。
class Typed(Validator):
expected_type = object
@classmethod
def check(cls, value):
if not isinstance(value, cls.expected_type):
raise TypeError(f'Expected {cls.expected_type}')
return super().check(value)
class Integer(Typed):
expected_type = int
class Float(Typed):
expected_type = float
class String(Typed):
expected_type = str
TypedクラスはValidatorのサブクラスです。expected_type属性を持ち、初期値はobjectに設定されています。Typedクラスのcheckメソッドは、与えられた値が期待される型であるかをチェックします。もしそうでなければ、TypeErrorを発生させます。型が正しければ、super().check(value)を使って親クラスのcheckメソッドを呼び出します。
Integer、Float、StringクラスはTypedを継承し、チェックする正確な型を指定しています。たとえば、Integerクラスは値が整数であるかをチェックします。
型検証器のテスト
型検証器を作成したので、テストしてみましょう。新しいターミナルを開き、以下のコマンドを実行して Python インタープリタを起動します。
python3
Python インタープリタが起動したら、検証器をインポートしてテストすることができます。以下のコードでテストします。
from validate import Integer, String
Integer.check(10) ## Should return 10
try:
Integer.check('10') ## Should raise TypeError
except TypeError as e:
print(f"Error: {e}")
String.check('10') ## Should return '10'
このコードを実行すると、以下のような出力が表示されるはずです。
10
Error: Expected <class 'int'>
'10'
これらの検証器を関数内で使用することもできます。試してみましょう。
def add(x, y):
Integer.check(x)
Integer.check(y)
return x + y
add(2, 2) ## Should return 4
try:
add('2', '3') ## Should raise TypeError
except TypeError as e:
print(f"Error: {e}")
このコードを実行すると、以下のような出力が表示されるはずです。
4
Error: Expected <class 'int'>
値検証器の追加
これまで、値の型をチェックする検証器を作成しました。次に、型ではなく値自体をチェックする検証器を追加しましょう。validate.pyファイルに戻り、以下のコードを追加します。
class Positive(Validator):
@classmethod
def check(cls, value):
if value < 0:
raise ValueError('Expected >= 0')
return super().check(value)
class NonEmpty(Validator):
@classmethod
def check(cls, value):
if len(value) == 0:
raise ValueError('Must be non-empty')
return super().check(value)
Positive検証器は、値が非負であるかをチェックします。値が 0 未満の場合、ValueErrorを発生させます。NonEmpty検証器は、値の長さがゼロでないかをチェックします。長さが 0 の場合、ValueErrorを発生させます。
多重継承による検証器の組み合わせ
次に、多重継承を使って検証器を組み合わせます。多重継承により、クラスは複数の親クラスから継承することができます。validate.pyファイルに戻り、以下のコードを追加します。
class PositiveInteger(Integer, Positive):
pass
class PositiveFloat(Float, Positive):
pass
class NonEmptyString(String, NonEmpty):
pass
これらの新しいクラスは、型チェックと値チェックを組み合わせています。たとえば、PositiveIntegerクラスは、値が整数であり、かつ非負であることをチェックします。ここでは継承の順序が重要です。検証器は、クラス定義で指定された順序でチェックされます。
組み合わせた検証器のテスト
組み合わせた検証器をテストしてみましょう。Python インタープリタで以下のコードを実行します。
from validate import PositiveInteger, PositiveFloat, NonEmptyString
PositiveInteger.check(10) ## Should return 10
try:
PositiveInteger.check('10') ## Should raise TypeError
except TypeError as e:
print(f"Error: {e}")
try:
PositiveInteger.check(-10) ## Should raise ValueError
except ValueError as e:
print(f"Error: {e}")
NonEmptyString.check('hello') ## Should return 'hello'
try:
NonEmptyString.check('') ## Should raise ValueError
except ValueError as e:
print(f"Error: {e}")
このコードを実行すると、以下のような出力が表示されるはずです。
10
Error: Expected <class 'int'>
Error: Expected >= 0
'hello'
Error: Must be non-empty
これは、検証器を組み合わせてより複雑な検証ルールを作成する方法を示しています。
テストが終了したら、以下のコマンドを実行して Python インタープリタを終了することができます。
exit()
検証器を株式クラスに適用する
このステップでは、実際のシチュエーションで検証器がどのように機能するかを見ていきます。検証器は、使用するデータが特定のルールを満たしていることを確認する小さなチェッカーのようなものです。Stockクラスを作成します。クラスはオブジェクトを作成するための青写真のようなものです。この場合、Stockクラスは株式市場の株を表し、検証器を使ってその属性の値(株数や価格など)が有効であることを確認します。
株式クラスの作成
まず、新しいファイルを作成する必要があります。WebIDE でstock.pyという名前の新しいファイルを作成します。このファイルにはStockクラスのコードが記述されます。では、stock.pyファイルに以下のコードを追加しましょう。
## stock.py
from validate import PositiveInteger, PositiveFloat
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def shares(self):
return self._shares
@shares.setter
def shares(self, value):
self._shares = PositiveInteger.check(value)
@property
def price(self):
return self._price
@price.setter
def price(self, value):
self._price = PositiveFloat.check(value)
def cost(self):
return self.shares * self.price
このコードが何をするかを分解して説明しましょう。
validateモジュールからPositiveIntegerとPositiveFloatの検証器をインポートします。これらの検証器は、株数が正の整数で、価格が正の浮動小数点数であることを確認するのに役立ちます。- 次に
Stockクラスを定義します。クラスの中には__init__メソッドがあります。このメソッドは、新しいStockオブジェクトを作成するときに呼び出されます。name、shares、priceの 3 つのパラメータを受け取り、それらをオブジェクトの属性に割り当てます。 - プロパティとセッターを使って、
sharesとpriceの値を検証します。プロパティは属性へのアクセスを制御する方法であり、セッターはその属性の値を設定しようとするときに呼び出されるメソッドです。shares属性を設定するとき、PositiveInteger.checkメソッドが呼び出されて、値が正の整数であることが確認されます。同様に、price属性を設定するとき、PositiveFloat.checkメソッドが呼び出されて、値が正の浮動小数点数であることが確認されます。 - 最後に、
costメソッドがあります。このメソッドは、株数に価格を掛けることで株の総コストを計算します。
株式クラスのテスト
Stockクラスを作成したので、検証器が正しく機能しているかをテストする必要があります。新しいターミナルを開き、Python インタープリタを起動します。以下のコマンドを実行することで行えます。
python3
Python インタープリタが起動したら、Stockクラスをインポートしてテストすることができます。Python インタープリタに以下のコードを入力します。
from stock import Stock
## Create a valid stock
s = Stock('GOOG', 100, 490.10)
print(f"Name: {s.name}, Shares: {s.shares}, Price: {s.price}")
print(f"Cost: {s.cost()}")
## Try setting an invalid shares value
try:
s.shares = -10
except ValueError as e:
print(f"Error setting shares: {e}")
## Try setting an invalid price value
try:
s.price = "not a price"
except TypeError as e:
print(f"Error setting price: {e}")
このコードを実行すると、以下のような出力が表示されるはずです。
Name: GOOG, Shares: 100, Price: 490.1
Cost: 49010.0
Error setting shares: Expected >= 0
Error setting price: Expected <class 'float'>
この出力は、検証器が期待通りに機能していることを示しています。Stockクラスは、sharesとpriceに無効な値を設定することを許可しません。無効な値を設定しようとすると、エラーが発生し、そのエラーをキャッチして表示することができます。
継承の役割の理解
検証器を使用する大きな利点の 1 つは、異なる検証ルールを簡単に組み合わせることができることです。継承は Python における強力な概念で、既存のクラスを基に新しいクラスを作成することができます。多重継承を使うと、super()関数を使って複数の親クラスのメソッドを呼び出すことができます。
たとえば、株式の名前が空でないことを確認したい場合は、以下の手順に従うことができます。
validateモジュールからNonEmptyString検証器をインポートします。この検証器は、株式の名前が空文字列でないことを確認するのに役立ちます。Stockクラスにname属性のプロパティセッターを追加します。このセッターは、NonEmptyString.check()メソッドを使って株式の名前を検証します。
これは、継承、特にsuper()関数を使った多重継承が、柔軟でさまざまな組み合わせで再利用できるコンポーネントを構築することを可能にすることを示しています。
テストが終了したら、以下のコマンドを実行して Python インタープリタを終了することができます。
exit()
まとめ
この実験では、Python における継承の動作について学び、いくつかの重要な概念を理解しました。単一継承と多重継承の違いを調べ、super()関数がメソッド解決順序(Method Resolution Order, MRO)をどのようにナビゲートするかを理解し、協調的な多重継承を実装する方法を学び、継承を使って実用的な検証システムを構築しました。
また、継承を使って柔軟な検証フレームワークを作成し、Stockクラスを用いた実際の例に適用しました。これは、継承がどのように再利用可能で組み合わせ可能なコンポーネントを作成するかを示しています。重要なポイントとしては、単一継承と多重継承におけるsuper()の動作、多重継承が機能を組み合わせる能力、および検証器を使ったプロパティセッターの使用法が挙げられます。これらの概念は、Python のオブジェクト指向プログラミングの基礎であり、実際のアプリケーションで広く使用されています。