関数を定義し、複数の場所から呼び出すことで、ソース コードをコピーして貼り付ける手間を省くことができます。コードを複製しないことは良い習慣です。コードを変更する必要がある場合 (バグの修正や新機能の追加など)、1 か所だけ変更すればよいからです。コードの繰り返しがなく、プログラムが短くなり、読みやすくなります。
関数と同様に、継承はクラスに適用できるコードの再利用手法です。これは、子クラスが親のメソッドのコピーを継承する親子関係にクラスを配置する行為であり、複数のクラスでメソッドを複製する必要がなくなります。
多くのプログラマーは、継承は過大評価され、危険でさえあると考えています。これは、継承されたクラスが多数あるとプログラムが複雑になるためです。「継承は悪」というタイトルのブログ投稿は、完全にとりとめのないものではありません; 継承は確かに乱用される傾向があります。ただし、この手法を限定的に使用すると、コードを整理するときに多くの時間を節約できます。
継承のしくみ
新しいサブクラスを作成するには、既存の親クラスの名前をclass
ステートメントの括弧内に入れます。サブクラス化を練習するには、新しいファイル エディター ウィンドウを開き、次のコードを入力します。名前を付けて保存しますinheritanceExample.py
。
class ParentClass: # 1
def printHello(self): # 2
print('Hello, world!')
class ChildClass(ParentClass): # 3
def someNewMethod(self):
print('ParentClass objects don't have this method.')
class GrandchildClass(ChildClass): # 4
def anotherNewMethod(self):
print('Only GrandchildClass objects have this method.')
print('Create a ParentClass object and call its methods:')
parent = ParentClass()
parent.printHello()
print('Create a ChildClass object and call its methods:')
child = ChildClass()
child.printHello()
child.someNewMethod()
print('Create a GrandchildClass object and call its methods:')
grandchild = GrandchildClass()
grandchild.printHello()
grandchild.someNewMethod()
grandchild.anotherNewMethod()
print('An error:')
parent.someNewMethod()
プログラムを実行すると、出力は次のようになります。
Create a ParentClass object and call its methods:
Hello, world!
Create a ChildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Create a GrandchildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Only GrandchildClass objects have this method.
An error:
Traceback (most recent call last):
File "inheritanceExample.py", line 35, in <module>
parent.someNewMethod() # ParentClass objects don't have this method.
AttributeError: 'ParentClass' object has no attribute 'someNewMethod'
ParentClass
1、3、4という名前の 3 つのクラスChildClass
を作成しました。サブクラスとは、とすべて同じメソッドを持つことを意味します。継承されたメソッドと言います。さらに、サブクラスはを継承しているため、親クラスと同じメソッドをすべて持っています。GrandchildClass
ChildClass
ParentClass
ChildClass
ParentClass
ChildClass
ParentClass
GrandchildClass
ChildClass
ChildClass
ParentClass
この手法を使用して、printHello()
方法 2 のコードをChildClass
andGrandchildClass
クラスに効果的にコピー アンド ペーストしました。printHello()
コードを変更すると、 だけでなくParentClass
と も更新されChildClass
ますGrandchildClass
。これは、関数内のコードを変更すると、そのすべての関数呼び出しが更新されるのと同じです。この関係を図 16-1 に示します。クラス ダイアグラムでは、矢印がサブクラスから基本クラスを指していることに注意してください。これは、クラスがその基本クラスを常に認識しているが、そのサブクラスを認識していないという事実を反映しています。
図 16-1: 3 つのクラスとそれらが持つメソッドの関係を示す階層図 (左) とベン図 (右)
親子クラスは「is-a」関係を表しているとよく言われます。オブジェクトChildClass
は、オブジェクトが定義するいくつかの追加メソッドを含め、オブジェクトと同じメソッドをすべてParentClass
持っているため、オブジェクトです。ParentClass
この関係は一方向です。ParentClass
オブジェクトはオブジェクトではありませんChildClass
。オブジェクト (およびのサブクラス)にのみ存在する をParentClass
オブジェクトが呼び出そうとすると、Python は をスローします。someNewMethod()
ChildClass
ChildClass
AttributeError
プログラマーは、関連するクラスが実際の "is" 階層に収まらなければならないと考えることがよくあります。通常、OOP チュートリアルにはVehicle``FourWheelVehicle▶``Car``Animal``Bird``Sparrow
、またはShape``Rectangle``Square
親クラス、子クラス、孫クラスがあります。ただし、継承の主な目的はコードの再利用であることを忘れないでください。プログラムで、他のクラスのメソッドの完全なスーパーセットである一連のメソッドを持つクラスが必要な場合、継承により、コードのコピーと貼り付けを回避できます。
また、サブクラスをサブクラスまたは派生クラス、親クラスをスーパークラスまたは基底クラスと呼ぶこともあります。
オーバーライド方法
サブクラスは、その親クラスのすべてのメソッドを継承します。ただし、サブクラスは、独自のメソッドに独自のコードを提供することで、継承されたメソッドをオーバーライドできます。サブクラスのオーバーライドされたメソッドは、スーパークラスのメソッドと同じ名前になります。
この概念を説明するために、前の章で作成した三目並べゲームに戻りましょう。今回は、より小さな三目並べボードを提供するために、MiniBoard
継承TTTBoard
とオーバーライドを行う新しいクラスを作成します。getBoardStr()
プログラムは、プレーヤーにどのスタイルのボードを使用するかを尋ねます。TTTBoard
残りのメソッドはMiniBoard
継承されるため、コピーして貼り付ける必要はありません。
tictactoe_oop.py
ファイルの末尾に次を追加して、元のTTTBoard
クラスをサブクラス化し、getBoardStr()
メソッドをオーバーライドします。
class MiniBoard(TTTBoard):
def getBoardStr(self):
"""Return a tiny text-representation of the board."""
# Change blank spaces to a '.'
for space in ALL_SPACES:
if self._spaces[space] == BLANK:
self._spaces[space] = '.'
boardStr = f'''
{
self._spaces['1']}{
self._spaces['2']}{
self._spaces['3']} 123
{
self._spaces['4']}{
self._spaces['5']}{
self._spaces['6']} 456
{
self._spaces['7']}{
self._spaces['8']}{
self._spaces['9']} 789'''
# Change '.' back to blank spaces.
for space in ALL_SPACES:
if self._spaces[space] == '.':
self._spaces[space] = BLANK
return boardStr
TTTBoard
クラスのgetBoardStr()
メソッドと同様に、MiniBoard
このメソッドは、関数に渡されたときに表示されるgetBoardStr()
三目並べボードの複数行の文字列を作成します。print()
しかし、この文字列ははるかに小さく、X トークンと O トークンの間の線を削除し、ドットを使用してスペースを表しています。
オブジェクトではなくオブジェクトmain()
をインスタンス化するように行を変更します。MiniBoard
TTTBoard
if input('Use mini board? Y/N: ').lower().startswith('y'):
gameBoard = MiniBoard() # Create a MiniBoard object.
else:
gameBoard = TTTBoard() # Create a TTTBoard object.
この 1 行の変更を除いてmain()
、プログラムの残りの部分は以前と同じです。ここでプログラムを実行すると、出力は次のようになります。
Welcome to Tic-Tac-Toe!
Use mini board? Y/N: y
... 123
... 456
... 789
What is X's move? (1-9)
1
X.. 123
... 456
... 789
What is O's move? (1-9)
`--snip--`
XXX 123
.OO 456
O.X 789
X has won the game!
Thanks for playing!
あなたのプログラムは、これら 2 つの三目並べボード クラスを簡単に実装できるようになりました。もちろん、ボードのミニ バージョンのみが必要な場合は、メソッドTTTBoard
内のコードを簡単に置き換えることができます。しかし、 と がgetBoardStr()
必要な場合は、継承により、共通のコードを再利用して 2 つのクラスを簡単に作成できます。
継承を使用していない場合は、という新しいプロパティをTTTBoard
追加しuseMiniBoard
、その中に通常のパネルまたはミニパネルをいつ表示するかを決定するステートメントをgetBoardStr()
入れることができます。if-else
このような単純な変更の場合、これはうまく機能します。しかし、MiniBoard
サブクラスが 2 つ、3 つ、または 100 個のメソッドをオーバーライドする必要がある場合はどうでしょうか? いくつかの異なるサブクラスを作成したい場合はどうすればよいでしょうかTTTBoard
? 継承を使用しないと、メソッドでステートメントが急増しif-else
、コードが大幅に複雑になります。サブクラスとオーバーライド メソッドを使用することで、コードを個別のクラスに整理して、これらのさまざまなユース ケースを処理できます。
super()
関数
サブクラスのオーバーライドされたメソッドは通常、スーパークラスのメソッドに似ています。継承はコードを再利用する手法ですが、メソッドをオーバーライドすると、親クラス メソッドの同じコードがサブクラス メソッドの一部としてオーバーライドされる可能性があります。このコードの重複を防ぐために、組み込みsuper()
関数を使用すると、オーバーライド メソッドが親クラスの元のメソッドを呼び出すことができます。
たとえば、のサブクラスでHintBoard
ある という新しいクラスを作成してみましょう。TTTBoard
新しいクラスは をオーバーライドするgetBoardStr()
ため、三目並べボードを描いた後、X または O が次の動きで勝った場合のヒントも追加されます。つまり、HintBoard
クラスのメソッドは、三目並べを描くクラスのメソッドと同じタスクをgetBoardStr()
すべて実行する必要があります。コードを複製する代わりに、クラスのメソッドからクラスのメソッドを呼び出すことでこれを行うことができます。ファイルの末尾に次を追加します。TTTBoard
getBoardStr()
super()
HintBoard
getBoardStr()
TTTBoard
getBoardStr()
tictactoe_oop.py
class HintBoard(TTTBoard):
def getBoardStr(self):
"""Return a text-representation of the board with hints."""
boardStr = super().getBoardStr() # Call getBoardStr() in TTTBoard. # 1
xCanWin = False
oCanWin = False
originalSpaces = self._spaces # Backup _spaces. # 2
for space in ALL_SPACES: # Check each space:
# Simulate X moving on this space:
self._spaces = copy.copy(originalSpaces)
if self._spaces[space] == BLANK:
self._spaces[space] = X
if self.isWinner(X):
xCanWin = True
# Simulate O moving on this space:
self._spaces = copy.copy(originalSpaces) # 3
if self._spaces[space] == BLANK:
self._spaces[space] = O
if self.isWinner(O):
oCanWin = True
if xCanWin:
boardStr += '\nX can win in one more move.'
if oCanWin:
boardStr += '\nO can win in one more move.'
self._spaces = originalSpaces
return boardStr
まず、 1 は親クラスsuper().getBoardStr()
内でコードを実行し、三目並べボードの文字列を返します。この文字列をという変数に一時的に保存します。このメソッドの残りのコードは、クラスを再利用して作成されたチェッカーボード文字列を使用して、ヒントの生成を処理します。次に、このメソッドはand変数を に設定し、ディクショナリを変数 2 としてバックアップします。次に、ループが からまでのすべてのボード スペースをループします。ループ内では、属性がディクショナリのコピーに設定され、ループされている現在のスペースが空の場合は、そこに X が配置されます。これは、この空きスペースでの X の次の移動をシミュレートします。への呼び出しは、これが勝ちの手であるかどうかを判断し、そうであれば、それを に設定します。次に、O についてこれらの手順を繰り返して、O がこのスペースで 3 を移動して勝つことができるかどうかを確認します。このメソッドはモジュールを使用して辞書をコピーするため、次の行を の先頭に追加します。TTTBoard
getBoardStr()
boardStr
TTTBoard
getBoardStr()
getBoardStr()
xCanWin
oCanWin
False
self._spaces
originalSpaces
for
'1'
'9'
self._spaces
originalSpaces
self.isWinner()
xCanWin
True
copy
self._spaces
tictactoe.py
import copy
次に、オブジェクトではなくオブジェクトをmain()
インスタンス化するように行を変更します。HintBoard
TTTBoard
gameBoard = HintBoard() # Create a TTT board object.
この 1 行の変更を除いてmain()
、プログラムの残りの部分は以前とまったく同じです。ここでプログラムを実行すると、出力は次のようになります。
Welcome to Tic-Tac-Toe!
`--snip--`
X| | 1 2 3
-+-+-
| |O 4 5 6
-+-+-
| |X 7 8 9
X can win in one more move.
What is O's move? (1-9)
5
X| | 1 2 3
-+-+-
|O|O 4 5 6
-+-+-
| |X 7 8 9
O can win in one more move.
`--snip--`
The game is a tie!
Thanks for playing!
メソッドの最後にtruexCanWin
の場合、追加のステートメント メッセージが文字列に追加されます。最後に、もう一度戻ってください。oCanWin
True
boardStr
boardStr
オーバーライドされたすべてのメソッドを使用する必要があるわけではありませんsuper()
。super()
クラスのオーバーライド メソッドが、親クラスのオーバーライドされたメソッドとはまったく異なることを行う場合、オーバーライドされたメソッドを呼び出す必要はありません。super()
関数は、この章の「多重継承」で説明するように、クラスに複数の親メソッドがある場合に特に役立ちます。
継承より合成を好む
継承は、コードを再利用するための優れた手法であり、おそらくすべてのクラスですぐに使い始めたいと思うでしょう。ただし、基本クラスとサブクラスを常に緊密に結合する必要があるとは限りません。複数レベルの継承を作成しても、コードに組織が追加されるのではなく、官僚主義が追加されます。
"is" 関係を持つクラス (つまり、サブクラスが一種の親クラスである場合) で継承を使用できますが、"is" 関係を持つクラスで構成と呼ばれる手法を使用すると有利な場合がよくあります。 コンポジションは、オブジェクトを継承するクラスではなく、クラス内にオブジェクトを含めるクラス設計手法です。これは、クラスにプロパティを追加するときに行うことです。継承を使用してクラスを設計する場合は、継承よりも構成を優先してください。以下の表で説明するように、これは、この章と前の章のすべての例で行ったことです。
- オブジェクトは、一定数のガレオン、シックル、およびクヌートのコインを「持っています」。
- オブジェクトは 9 個のスペースのセットを「持っています」。
- オブジェクト
MiniBoard
はオブジェクトなTTTBoard
ので、9 つのスペースのセットも「持っています」。 - オブジェクト
HintBoard
はオブジェクトなTTTBoard
ので、9 つのスペースのセットも「持っています」。
WizCoin
前の章のクラスに戻りましょう。WizardCustomer
魔法界の顧客を表す新しいクラスを作成すると、これらの顧客は一定の金額を持ち、WizCoin
クラスを通じてこのお金を表すことができます。しかし、2 つのクラスの間には「1 つである」という関係はなく、WizardCustomer
オブジェクトはオブジェクトではありませんWizCoin
。継承を使用すると、ぎこちないコードになる可能性があります。
import wizcoin
class WizardCustomer(wizcoin.WizCoin): # 1
def __init__(self, name):
self.name = name
super().__init__(0, 0, 0)
wizard = WizardCustomer('Alice')
print(f'{
wizard.name} has {
wizard.value()} knuts worth of money.')
print(f'{
wizard.name}\'s coins weigh {
wizard.weightInGrams()} grams.')
この例では、や などの 1 オブジェクトのメソッドWizardCustomer
を継承しています。技術的には、から継承すると、オブジェクトをプロパティとして含めるのと同じタスクを実行できます。しかし、メソッドとメソッドの名前は誤解を招きます。それらは、魔法使いのコインの値と重さではなく、魔法使いの値と重さを返すようです。また、後でウィザードの weight にメソッドを追加したい場合、そのメソッド名は既に取得されています。WizCoin
value()
weightInGrams()
WizCoin
WizardCustomer
WizCoin
WizardCustomer
wizard.value()
wizard.weightInGrams()
weightInGrams()
WizCoin
ウィザードの顧客は一定量のウィザード コインを「持っている」ため、オブジェクトをプロパティとして持つ方が適切です。
import wizcoin
class WizardCustomer:
def __init__(self, name):
self.name = name
self.purse = wizcoin.WizCoin(0, 0, 0) # 1
wizard = WizardCustomer('Alice')
print(f'{
wizard.name} has {
wizard.purse.value()} knuts worth of money.')
print(f'{
wizard.name}\'s coins weigh {
wizard.purse.weightInGrams()} grams.')
WizardCustomer
クラスに からメソッドを継承させる代わりに、オブジェクトを含む属性 1 をクラスにWizCoin
与えました。コンポジションを使用する場合、クラスのメソッドを変更しても、クラスのメソッドは変更されません。この手法により、2 つのクラスの将来の設計変更に対する柔軟性が向上し、コードがより保守しやすくなります。WizardCustomer
purse
WizCoin
WizCoin
WizardCustomer
継承された短所
継承の主な欠点は、親クラスに加えた将来の変更が、そのすべてのサブクラスに継承されることです。ほとんどの場合、この密結合はまさにあなたが望むものです。ただし、場合によっては、コードのニーズが継承モデルに完全に適合しないことがあります。
たとえば、車両シミュレーション プログラムにCar
とクラスMotorcycle
があるとしますLunarRover
。startIgnition()
それらはすべて、や などの同様のメソッドを必要としますchangeTire()
。このコードをコピーして各クラスに貼り付ける代わりに、親クラスを作成してVehicle
、Car
、 、Motorcycle
およびそれを継承することができます。LunarRover
これで、たとえばメソッドのバグを修正する必要がある場合changeTire()
、1 か所を変更するだけで済みます。Vehicle
これは、継承元のさまざまな車両関連のクラスが多数ある場合に特に便利です。これらのクラスのコードは次のようになります。
class Vehicle:
def __init__(self):
print('Vehicle created.')
def startIgnition(self):
pass # Ignition starting code goes here.
def changeTire(self):
pass # Tire changing code goes here.
class Car(Vehicle):
def __init__(self):
print('Car created.')
class Motorcycle(Vehicle):
def __init__(self):
print('Motorcycle created.')
class LunarRover(Vehicle):
def __init__(self):
print('LunarRover created.')
ただし、今後のすべてのVehicle
変更は、これらのサブクラスにも影響します。メソッドが必要な場合はどうしますかchangeSparkPlug()
?車やオートバイにはスパークプラグ付きの内燃エンジンがありますが、月面車にはありません。CombustionEngine
継承よりも構成を優先することで、個人とElectricEngine
クラスを作成できます。次に、CombustionEngine ElectricEngineVehicle
を「持つ」ようにクラスを設計しました。engine attribute, either a
或
对象,使用适当的方法:
class CombustionEngine:
def __init__(self):
print('Combustion engine created.')
def changeSparkPlug(self):
pass # Spark plug changing code goes here.
class ElectricEngine:
def __init__(self):
print('Electric engine created.')
class Vehicle:
def __init__(self):
print('Vehicle created.')
self.engine = CombustionEngine() # Use this engine by default.
`--snip--`
class LunarRover(Vehicle):
def __init__(self):
print('LunarRover created.')
self.engine = ElectricEngine()
Vehicle
これには、特に既存のクラスから継承する複数のクラスがある場合、多くのコードの書き直しが必要になる可能性があります。Vehicle
クラスまたはそのサブクラスの各オブジェクトに対して、すべてのvehicleObj.changeSparkPlug()
呼び出しを に変換する必要がありますvehicleObj.engine.changeSparkPlug()
。LunarVehicle
このような大きな変更によりエラーが発生する可能性があるため、単純にメソッドに何も行わせないことをお勧めしますchangeSparkPlug()
。この場合、Python スタイルのアプローチは、LunarVehicle
クラス内で次のようにchangeSparkPlug
設定することですNone
。
class LunarRover(Vehicle):
changeSparkPlug = None
def __init__(self):
print('LunarRover created.')
changeSparkPlug = None
行は、この章で後述する「クラス属性」で説明する構文に従います。これはからVehicle
継承されたメソッドをオーバーライドするchangeSparkPlug()
ため、LunarRover
オブジェクトで呼び出すとエラーが発生します。
>>> myVehicle = LunarRover()
LunarRover created.
>>> myVehicle.changeSparkPlug()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable
LunarRover
このエラーにより、オブジェクトでこの不適切なメソッドを呼び出そうとした場合に、すぐに失敗して問題をすぐに見つけることができます。サブクラスLunarRover
もchangeSparkPlug()
このNone
値を継承します。TypeError: 'NoneType' object is not callable
エラー メッセージは、LunarRover
クラスのプログラマーが意図的にchangeSparkPlug()
メソッドを に設定したことを示していますNone
。そもそもそのようなメソッドがなかった場合、NameError: name 'changeSparkPlug' is not defined
エラー メッセージが表示されます。
継承によって、複雑で矛盾するクラスが作成される可能性があります。多くの場合、代わりにテキストを使用する方が有利です。
isinstance()
とissubclass()
関数
type()
オブジェクトの型を知る必要がある場合、前の章で説明したように、オブジェクトを組み込み関数に渡すことができます。ただし、オブジェクトの型チェックを行う場合は、isinstance()
より柔軟な組み込み関数を使用することをお勧めします。オブジェクトが指定されたクラスまたは指定されたクラスのサブクラスに属している場合、関数は戻ります。インタラクティブ シェルに次のように入力します。isinstance()
True
>>> class ParentClass:
... pass
...
>>> class ChildClass(ParentClass):
... pass
...
>>> parent = ParentClass() # Create a ParentClass object.
>>> child = ChildClass() # Create a ChildClass object.
>>> isinstance(parent, ParentClass)
True
>>> isinstance(parent, ChildClass)
False
>>> isinstance(child, ChildClass) # 1
True
>>> isinstance(child, ParentClass) # 2
True
isinstance()
表現child
内のオブジェクトは、 1 のインスタンスと2 のインスタンスChildClass
であることに注意してください。オブジェクトは一種のオブジェクトであるため、これは理にかなっています。ChildClass
ParentClass
ChildClass
ParentClass
クラス オブジェクトのタプルを 2 番目の引数として渡して、最初の引数がタプル内のいずれかのクラスであるかどうかを確認することもできます。
>>> isinstance(42, (int, str, bool)) # True if 42 is an int, str, or bool.
True
あまり一般的ではないissubclass()
組み込み関数は、最初の引数として渡されたクラス オブジェクトが 2 番目の引数として渡されたクラス オブジェクトのサブクラス (または同じクラス) であるかどうかを認識します。
>>> issubclass(ChildClass, ParentClass) # ChildClass subclasses ParentClass.
True
>>> issubclass(ChildClass, str) # ChildClass doesn't subclass str.
False
>>> issubclass(ChildClass, ChildClass) # ChildClass is ChildClass.
True
use と同様にisinstance()
、クラス オブジェクトのタプルを 2 番目の引数として渡してissubclass()
、最初の引数がタプル内のいずれかのクラスのサブクラスであるかどうかを確認できます。isinstance()
との主な違いは、2 つのクラス オブジェクトが渡されるのに対し、オブジェクトとクラス オブジェクトが渡されることissubclass()
です。issubclass()
isinstance()
クラスメソッド
クラス メソッドは、通常のメソッドのように単一のオブジェクトではなく、クラスに関連付けられます。次の例に示すようにdef
、メソッドのステートメントの前の@classmethod
デコレーターと最初のパラメーターとしての使用の 2 つのトークンが表示されると、コード内のクラス メソッドを認識できます。cls
class ExampleClass:
def exampleRegularMethod(self):
print('This is a regular method.')
@classmethod
def exampleClassMethod(cls):
print('This is a class method.')
# Call the class method without instantiating an object:
ExampleClass.exampleClassMethod()
obj = ExampleClass()
# Given the above line, these two lines are equivalent:
obj.exampleClassMethod()
obj.__class__.exampleClassMethod()
パラメータは、オブジェクトのクラスではなくself
オブジェクトを参照することを除いて、同様に動作します。つまり、クラス メソッド内のコードは、個々のオブジェクト プロパティにアクセスしたり、オブジェクトの通常のメソッドを呼び出したりすることはできません。クラス メソッドは、他のクラス メソッドを呼び出すか、クラス属性にアクセスすることしかできません。名前は Python のキーワードであるため使用します。 、 、などの他のキーワードと同様に、パラメーター名には使用できません。のように、クラス オブジェクトを介してクラス属性を呼び出すことがよくあります。しかし、 のように、クラスの任意のオブジェクトを介してそれらを呼び出すこともできます。cls
cls
self
cls
class
if
while
import
ExampleClass.exampleClassMethod()
obj.exampleClassMethod()
クラス メソッドは一般的に使用されません。最も一般的な使用例は、__init__()
以外のオプションのコンストラクターを提供することです。たとえば、新しいオブジェクトが必要とするデータの文字列と、新しいオブジェクトが必要とするデータを含むファイル名の文字列の両方をコンストラクターが受け入れることができるとしたら? __init__()
メソッドの長くて紛らわしいパラメーター リストは必要ありません。代わりに、新しいオブジェクトを返すクラス メソッドを使用しましょう。
たとえば、AsciiArt
クラスを作成してみましょう。第 14 章で見たように、アスキー アートはテキスト文字を使用してイメージを形成します。
class AsciiArt:
def __init__(self, characters):
self._characters = characters
@classmethod
def fromFile(cls, filename):
with open(filename) as fileObj:
characters = fileObj.read()
return cls(characters)
def display(self):
print(self._characters)
# Other AsciiArt methods would go here...
face1 = AsciiArt(' _______\n' +
'| . . |\n' +
'| \\___/ |\n' +
'|_______|')
face1.display()
face2 = AsciiArt.fromFile('face.txt')
face2.display()
AsciiArt
クラスには、__init__()
画像のテキスト文字を文字列として渡すことができるメソッドがあります。また、fromFile()
ASCII アートを含むテキスト ファイルのファイル名文字列を渡すことができるクラス メソッドもあります。どちらのメソッドもAsciiArt
オブジェクトを作成します。
このプログラムを実行して ASCII アート サーフェスを含むファイルを作成するとface.txt
、出力は次のようになります。
_______
| . . |
| \___/ |
|_______|
_______
| . . |
| \___/ |
|_______|
__init__()
クラス メソッドを使用すると、すべてを実行するよりもfromFile()
コードが読みやすくなります。
AsciiArt
クラス メソッドのもう 1 つの利点は、クラスのサブクラスがそのメソッドを継承できる(必要に応じてオーバーライドできる)ことですfromFile()
。そのため、の代わりにAsciiArt
クラスのfromFile()
メソッドでcls
呼び出します。クラスはメソッドにハードコードされていないため、呼び出しは変更なしで のサブクラスでも機能します。ただし、呼び出しは常に、サブクラスの ではなく、クラスのを呼び出します。「このクラスを表すオブジェクト」として理解できます(characters)
AsciiArt(characters)
cls()
AsciiArt
AsciiArt
AsciiArt()
AsciiArt
__init__()
__init__()
cls
通常のメソッドが常にコード内のどこかでパラメーターを使用する必要があるのと同様にself
、クラス メソッドは常にそのcls
パラメーターを使用する必要があることに注意してください。クラス メソッドのコードがパラメーターをまったく使用しない場合cls
、これはクラス メソッドがおそらく単なる関数であることを示しています。
クラス属性
クラス属性は、オブジェクトではなくクラスに属する変数です。ファイル内ですべての関数の外にグローバル変数を作成するのと同じように、.py
クラス内ですべてのメソッドの外でクラス属性を作成します。作成されたオブジェクトのcount
数を追跡する という名前のクラス属性の例を次に示します。CreateCounter
class CreateCounter:
count = 0 # This is a class attribute.
def __init__(self):
CreateCounter.count += 1
print('Objects created:', CreateCounter.count) # Prints 0.
a = CreateCounter()
b = CreateCounter()
c = CreateCounter()
print('Objects created:', CreateCounter.count) # Prints 3.
CreateCounter
クラスには、count
という名前のクラス属性があります。すべてのCreateCounter
オブジェクトは、独自の個別のcount
プロパティを持つ代わりに、このプロパティを共有します。そのため、コンストラクターの行は、CreateCounter.count += 1
作成される各オブジェクトを記録できますCreateCounter
。プログラムを実行すると、出力は次のようになります。
Objects created: 0
Objects created: 3
クラス属性はめったに使用しません。この「作成されたオブジェクトの数をカウントするCreateCounter
」例でさえ、クラス プロパティの代わりにグローバル変数を使用することで、より簡単に実行できます。
静的メソッド
静的メソッドにはself
orパラメーターがありませんcls
。静的メソッドは、クラスまたはそのオブジェクトのプロパティまたはメソッドにアクセスできないという点で、実際には単なる関数です。Python では、静的メソッドが必要になることはめったにありません。関数を使用する場合は、通常の関数の作成を検討する必要があります。
静的メソッドのステートメントの前にデコレータをdef
配置して、静的メソッドを定義します。@staticmethod
以下は静的メソッドの例です。
class ExampleClassWithStaticMethod:
@staticmethod
def sayHello():
print('Hello!')
# Note that no object is created, the class name precedes sayHello():
ExampleClassWithStaticMethod.sayHello()
ExampleClassWithStaticMethod
クラス内のsayHello()
静的メソッドと関数sayHello()
の間にはほとんど違いはありません。実際、事前にクラス名を入力しなくても関数を呼び出すことができるため、関数を使用することをお勧めします。
静的メソッドは、Python の柔軟な言語機能を持たない他の言語でより一般的です。Python には、他の言語の機能を模倣する静的メソッドが含まれていますが、実用的な価値はほとんどありません。
オブジェクト指向クラスと静的属性を使用する場合
クラス メソッド、クラス プロパティ、および静的メソッドが必要になることはほとんどありません。また、過度に使用する傾向があります。「代わりに関数やグローバル変数を使用できないのはなぜですか?」と考えている場合は、おそらくクラス メソッド、クラス プロパティ、または静的メソッドを使用する必要がないというヒントです。この中級レベルの本でそれらを紹介する唯一の理由は、コードでそれらに遭遇したときにそれらを認識できるようにするためですが、それらを使用しないことをお勧めします. これらは、フレームワークを使用するプログラマーによってサブクラス化される一連の慎重に作成されたクラスで独自のフレームワークを作成する場合に役立ちます。しかし、単純な Python アプリケーションを作成している場合は、おそらくそれらは必要ありません。
これらの機能の詳細と、それらが必要な理由と必要でない理由については、Phillip J. Eby のdirtsimple.org/2004/12/python-is-not-java.html
記事「Python は Java ではない」と Ryan Tomaykotomayko.com/blog/2004/the-static-method-thing
の記事「静的メソッド」を参照してください。
オブジェクト指向のバズワード
通常、OOP の説明は、継承、カプセル化、ポリモーフィズムなど、いくつかの用語から始まります。これらの用語を知ることの重要性は過大評価されていますが、少なくとも基本的な理解が必要です。継承については既に説明したので、ここではその他の概念について説明します。
カプセル化
カプセル化という言葉には、一般的ではあるが関連する定義が 2 つあります。最初の定義は、カプセル化とは、関連するデータとコードを 1 つのユニットにまとめることです。カプセル化するとは、ボックス化することを意味します。クラスは、関連するプロパティとメソッドをグループ化します。たとえば、このWizCoin
クラスは 3 つの整数knuts
とsickles
をgalleons
1 つのオブジェクトにカプセル化しますWizCoin
。
2 番目の定義は、カプセル化は、オブジェクトがどのように機能するかに関する複雑な実装の詳細をオブジェクトが隠蔽できるようにする情報隠蔽技術であるということです。これについては、282 ページの「プライベート プロパティとメソッド」で確認できます。ここでは、BankAccount
オブジェクトは、プロパティの処理方法の詳細を非表示にするdeposit()
とメソッドを提供します。関数は同様のブラック ボックスの目的を果たします。関数が数値の平方根を計算する方法は隠されています。関数は、渡した数値の平方根を返すことを知っておく必要があります。withdraw()
_balance
math.sqrt()
ポリモーフィズム
ポリモーフィズムにより、あるタイプのオブジェクトを別のタイプのオブジェクトとして扱うことができます。たとえば、len()
関数は渡された引数の長さを返します。文字列を渡してlen()
文字数を確認できますが、リストまたは辞書を渡して、len()
エントリまたはキーと値のペアの数をそれぞれ確認することもできます。この形式のポリモーフィズムは、さまざまな種類のオブジェクトを処理できるため、ジェネリック関数またはパラメトリック ポリモーフィズムとして知られています。
ポリモーフィズムは、アドホック ポリモーフィズムまたは演算子のオーバーロードも指します。演算子 ( または+
など*
) は、操作対象のオブジェクトのタイプに応じて異なる動作をする可能性があります。たとえば、+
演算子は、2 つの整数値または浮動小数点値を処理する場合は数学的加算を行いますが、2 つの文字列を処理する場合は文字列連結を行います。演算子のオーバーロードについては、第 17 章で説明します。
継承を使用しない場合
継承を使用してクラスをオーバーエンジニアリングするのは簡単です。Luciano Ramaljo が言ったように、「物事を整理することは、私たちの秩序感覚を刺激します。プログラマーは楽しみのためにそれを行います。」クラスまたはモジュール内の複数の関数が同じことを達成できる場合、クラス、サブクラス、およびサブクラスを作成します。しかし、第 6 章の Python マントラを思い出してください: シンプルは複雑よりも優れています。
py
OOP を使用すると、特定の順序で定義されていない数百の関数を含む 1 つの大きなファイルよりも簡単に推論できる小さな単位 (この場合はクラス) にコードを編成できます。継承は、すべてが同じディクショナリまたはリスト データ構造で動作する複数の関数がある場合に便利です。この場合、それらをクラスに編成すると便利です。
ただし、クラスの作成や継承の使用を必要としない例をいくつか示します。
self
またはパラメータを使用しないメソッドでクラスが構成されている場合はcls
、そのクラスを削除し、メソッドの代わりに関数を使用します。- サブクラスが 1 つしかない親クラスを作成したが、親クラスのオブジェクトを作成したことがない場合は、それらを 1 つのクラスに結合できます。
- 3 つまたは 4 つを超えるレベルのサブクラスを作成すると、不必要に継承を使用している可能性があります。これらのサブクラスをより少ないクラスにマージします。
前の章で示した三目並べプログラムの非オブジェクト指向バージョンとオブジェクト指向バージョンのように、クラスを使用しなくても、動作するエラーのないプログラムを作成することはもちろん可能です。プログラムをクラスの複雑なネットワークに設計する必要があるとは思わないでください。機能する単純なソリューションは、機能しない複雑なソリューションよりも優れています。Joel Spolsky はブログ記事「Don't Let Let Astronaut Architects Scare You」に書いています。
継承などのオブジェクト指向の概念がどのように機能するかを知っておく必要があります。これは、コードを整理し、開発とデバッグを容易にするのに役立ちます。Python の柔軟性により、この言語は OOP 機能を提供するだけでなく、プログラムのニーズに合わない場合でもそれらを使用する必要がありません。
多重継承
多くのプログラミング言語では、クラスを最大 1 つの親クラスに制限しています。Python は、多重継承と呼ばれる機能を提供することで、複数の親クラスをサポートします。たとえば、flyInTheAir()
メソッドを持つクラスとメソッドをAirplane
持つクラスを持つことができます。次に、ステートメントに 2 つをコンマで区切ってリストすることにより、 andを継承するクラスを作成できます。新しいファイル エディター ウィンドウを開き、次のファイルを名前を付けて保存します。floatOnWater()
Ship
Airplane
Ship
FlyingBoat
class
flyingboat.py
class Airplane:
def flyInTheAir(self):
print('Flying...')
class Ship:
def floatOnWater(self):
print('Floating...')
class FlyingBoat(Airplane, Ship):
pass
インタラクティブ シェルで確認できるように、作成するオブジェクトはおよびメソッドFlyingBoat
を継承します。flyInTheAir()
floatOnWater()
>>> from flyingboat import *
>>> seaDuck = FlyingBoat()
>>> seaDuck.flyInTheAir()
Flying...
>>> seaDuck.floatOnWater()
Floating...
親クラスのメソッド名が異なり、重複しない限り、多重継承は単純です。これらの種類のクラスはmixinと呼ばれます。(これは単なるクラスの一般的な用語です。Python にはキーワードがありませんmixin
。) しかし、メソッド名を共有する複数の複雑なクラスから継承するとどうなるでしょうか?
たとえば、この章の前半のMiniBoard
Tic Tac Toe ボード クラスについて考えてみましょうHintTTTBoard
。ミニチュアの三目並べボードを表示してヒントを与えるクラスが必要だとしたら? 多重継承により、これらの既存のクラスを再利用できます。次をtictactoe_oop.py
ファイルの最後に追加しますが、main()
関数を呼び出すステートメントの前に追加しますif
。
class HybridBoard(HintBoard, MiniBoard):
pass
このクラスには何もありません。継承HintBoard
とMiniBoard
. 次に、main()
オブジェクトを作成するように関数のコードを変更しますHybridBoard
。
gameBoard = HybridBoard() # Create a TTT board object.
両方の親クラスMiniBoard
とHintBoard
両方にgetBoardStr()
というメソッドがあるため、HybridBoard
どちらから継承するか? このプログラムを実行すると、出力にはミニチュアの三目並べが表示されますが、ヒントも提供されます。
`--snip--`
X.. 123
.O. 456
X.. 789
X can win in one more move.
Python は魔法のようにMiniBoard
クラスgetBoardStr()
メソッドとHintBoard
クラスgetBoardStr()
メソッドをマージして両方を実現しているようです! しかし、それは私がそれらを相互に作用するように書いたからです。実際、HybridBoard
classclass
ステートメントでクラスの順序を変更すると、次のようになります。
`class HybridBoard(MiniBoard, HintBoard):`
ヒントを完全に失います:
`--snip--`
X.. 123
.O. 456
X.. 789
この理由を理解するには、Python のメソッド解決順序( MRO ) と、super()
関数が実際にどのように機能するかを理解する必要があります。
メソッド解決順序
図 16-2 に示すように、tic-tac-toe プログラムにはボードを表す 4 つのクラスがあり、そのうちの 3 つはgetBoardStr()
メソッドが定義されており、1 つはメソッドが継承されています。getBoardStr()
図 16-2: 三目並べプログラムの 4 つのクラス
HybridBoard
objectで呼び出すとgetBoardStr()
、Python はHybridBoard
クラスにその名前のメソッドがないことを認識しているため、その親クラスをチェックします。しかし、クラスには 2 つの親があり、両方ともgetBoardStr()
メソッドを持っています。どれが呼ばれますか?
HybridBoard
クラスの MRO を調べることで見つけることができます。これはsuper()
、Python がメソッドを継承するとき、またはメソッドが関数を呼び出すときにチェックするクラスの順序付きリストです。HybridBoard
クラスmro()
の MRO は、インタラクティブ シェルでメソッドを呼び出すことで確認できます。
>>> from tictactoe_oop import *
>>> HybridBoard.mro()
[<class 'tictactoe_oop.HybridBoard'>, <class 'tictactoe_oop.HintBoard'>, <class 'tictactoe_oop.MiniBoard'>, <class 'tictactoe_oop.TTTBoard'>, <class 'object'>]
この戻り値からわかるように、 でHybridBoard
メソッドを呼び出すと、Python は最初にHybridBoard
クラス内でそれをチェックします。存在しない場合、Python はHintBoard
クラス、次にMiniBoard
クラス、最後にTTTBoard
クラスをチェックします。各 MRO リストの最後には組み込みobject
クラスがあり、これは Python のすべてのクラスの親クラスです。
単一継承では、MRO の決定は簡単です。親クラスのチェーンを作成するだけです。多重継承では、よりトリッキーになります。Python の MRO は C3 アルゴリズムに従いますが、その詳細はこの本の範囲を超えています。ただし、次の 2 つのルールを覚えておくことで、MRO を決定できます。
- Python は、スーパークラスをチェックする前にサブクラスをチェックします。
- Python は、
class
ステートメントの左から右にリストされている継承されたクラスを調べます。
HybridBoard
object を呼び出すとgetBoardStr()
、Python は最初にHybridBoard
クラスをチェックします。次に、クラスの親は sum from left to HintBoard
rightであるためMiniBoard
、Python は をチェックしますHintBoard
。この親クラスにはgetBoardStr()
メソッドがあるので、HybridBoard
継承して呼び出します。
しかし、それだけではありません。次に、メソッド call ですsuper().getBoardStr()
。super
は、親クラスを返す代わりに MRO の次のクラスを返すため、 Pythonsuper()
の関数のやや誤解を招く名前です。これは、HybridBoard
object を呼び出すとgetBoardStr()
、その MRO 内の次のクラスであるHintBoard
after はMiniBoard
親クラスではなく、 であることを意味しますTTTBoard
。つまり、ミニチュアの三目並べ文字列を返すクラスメソッドsuper().getBoardStr()
の呼び出しです。この呼び出しの後のクラスの残りのコードは、プロンプト テキストをこの文字列に追加します。MiniBoard
getBoardStr()
super()
HintBoard
getBoardStr()
HybridBoard
classclass
ステートメントを最初にリストされるように変更するとMiniBoard
、HintBoard
その MRO が最初にMiniBoard
リストされますHintBoard
。これは、を呼び出さずにHybridBoard
からMiniBoard
継承することを意味します。この順序付けにより、ミニチュアの三目並べボードにサイレント エラーが表示されます。getBoardStr()
MiniBoard
super()
super()
MiniBoard
getBoardStr()
HintBoard
getBoardStr()
多重継承を使用すると、少量のコードで多くの機能を作成できますが、過度に設計された、理解しにくいコードに簡単につながる可能性があります。単一継承、ミックスイン クラス、または継承なしをサポートします。これらの手法は、多くの場合、プログラムのタスクをより適切に達成できます。
要約する
継承は、コードの再利用手法です。親クラスのメソッドを継承するサブクラスを作成できます。super()
これらのメソッドをオーバーライドして新しいコードを与えることができますが、関数を使用して親クラスの元のメソッドを呼び出すこともできます。サブクラスのオブジェクトは親クラスのオブジェクトの型であるため、サブクラスには親クラスとの「is」関係があります。
Python では、クラスと継承の使用はオプションです。一部のプログラマーは、継承を多用することの複雑さに見合うだけの価値があるとは考えていません。継承ではなく構成を使用すると、他のクラスからメソッドを直接継承するのではなく、あるクラスのオブジェクトと他のクラスのオブジェクトとの「has」関係を実装するため、多くの場合より柔軟になります。これは、あるクラスのオブジェクトが別のクラスのオブジェクトを所有できることを意味します。たとえば、Customer
オブジェクトには、クラスのサブクラスではなくDate
オブジェクトに割り当てられたプロパティがある場合があります。birthdate
Customer
Date
type()
渡されたオブジェクトの型を返すことができるように、isinstance()
関数issubclass()
は渡されたオブジェクトの型と継承情報を返します。
クラスはオブジェクト メソッドとプロパティを持つことができますが、クラス メソッド、クラス プロパティ、および静的メソッドを持つこともできます。これらはめったに使用されませんが、グローバル変数や関数ではサポートできない他のオブジェクト指向の手法をサポートできます。
Python では、クラスが複数の親クラスから継承できるようになっていますが、これによりコードがわかりにくくなる可能性があります。super()
クラスの関数とメソッドは、MRO に基づいてメソッドがどのように継承されるかを決定します。mro()
クラスのメソッドを呼び出すことにより、対話型シェルでクラスの MRO を表示できます。
この章と前の章では、一般的な OOP の概念について説明しました。次の章では、Python 固有の OOP 手法について説明します。`