継承による拡張可能なプログラム

PythonPythonBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

継承は、拡張可能なプログラムを書くために一般的に使用されるツールです。このセクションでは、その考え方を探ります。

継承

既存のオブジェクトを特殊化するために継承が使用されます。

class Parent:
  ...

class Child(Parent):
  ...

新しいクラス Child は、派生クラスまたはサブクラスと呼ばれます。Parent クラスは、基底クラスまたはスーパークラスとして知られています。Parent は、クラス名の後の () 内に指定されます。class Child(Parent):

拡張

継承を使うことで、既存のクラスを使って以下のことができます。

  • 新しいメソッドを追加する
  • 既存のメソッドの一部を再定義する
  • インスタンスに新しい属性を追加する

最終的には、既存のコードを拡張します。

これがあなたの出発点となるクラスだとしましょう。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

これを継承を通じて任意の部分を変更することができます。

新しいメソッドを追加する

class MyStock(Stock):
    def panic(self):
        self.sell(self.shares)

使用例。

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.sell(25)
>>> s.shares
75
>>> s.panic()
>>> s.shares
0
>>>

既存のメソッドを再定義する

class MyStock(Stock):
    def cost(self):
        return 1.25 * self.shares * self.price

使用例。

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.cost()
61262.5
>>>

新しいメソッドは古いメソッドの代わりを果たします。他のメソッドは影響を受けません。すごいですね。

オーバーライド

時には、クラスが既存のメソッドを拡張しますが、再定義の中で元の実装を使用したい場合があります。このために、super() を使用します。

class Stock:
  ...
    def cost(self):
        return self.shares * self.price
  ...

class MyStock(Stock):
    def cost(self):
        ## `super` への呼び出しを確認
        actual_cost = super().cost()
        return 1.25 * actual_cost

前のバージョンを呼び出すには super() を使用します。

注意: Python 2 では、構文がもっと冗長でした。

actual_cost = super(MyStock, self).cost()

__init__ と継承

__init__ が再定義される場合、親を初期化することが不可欠です。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

class MyStock(Stock):
    def __init__(self, name, shares, price, factor):
        ## `super` と `__init__` への呼び出しを確認
        super().__init__(name, shares, price)
        self.factor = factor

    def cost(self):
        return self.factor * super().cost()

前に示したように、前のバージョンを呼び出す方法である super__init__() メソッドを呼び出す必要があります。

継承の使用

継承は、時には関連するオブジェクトを整理するために使用されます。

class Shape:
 ...

class Circle(Shape):
 ...

class Rectangle(Shape):
 ...

論理的な階層構造や分類体系を考えてみてください。ただし、より一般的(かつ実用的)な使い方は、再利用可能または拡張可能なコードを作成することに関連しています。たとえば、フレームワークが基底クラスを定義し、それをカスタマイズするよう指示する場合があります。

class CustomHandler(TCPHandler):
    def handle_request(self):
     ...
        ## カスタム処理

基底クラスにはいくつかの汎用的なコードが含まれています。あなたのクラスはそれを継承し、特定の部分をカスタマイズします。

「is a」関係

継承は型関係を確立します。

class Shape:
...

class Circle(Shape):
...

オブジェクトインスタンスを確認します。

>>> c = Circle(4.0)
>>> isinstance(c, Shape)
True
>>>

重要: 理想的には、親クラスのインスタンスで機能する任意のコードは、子クラスのインスタンスでも機能するはずです。

object 基底クラス

クラスに親がない場合、時には object が基底として使用されることがあります。

class Shape(object):
...

object は Python のすべてのオブジェクトの親です。

*注: 技術的には必要ありませんが、Python 2 での必須の使用からの残りとして指定されるのをよく見ます。省略した場合でも、クラスは依然として暗黙的に object から継承します。

多重継承

クラスの定義で複数のクラスを指定することで、複数のクラスから継承することができます。

class Mother:
...

class Father:
...

class Child(Mother, Father):
...

Child クラスは両親の特徴を継承します。いくつかかなり厄介な詳細があります。何をしているかを知っている場合を除いてはやめてください。次のセクションでさらに情報を提供しますが、このコースでは多重継承をさらに利用しません。

継承の主な用途は、さまざまな方法で拡張またはカスタマイズすることを目的としたコードの記述です。特にライブラリやフレームワークです。例として、report.py プログラムの print_report() 関数を考えてみましょう。おそらくこんな感じになっているはずです。

def print_report(reportdata):
    '''
    (名前, 株数, 価格, 変動) のタプルのリストから、見やすくフォーマットされたテーブルを表示します。
    '''
    headers = ('Name','Shares','Price','Change')
    print('%10s %10s %10s %10s' % headers)
    print(('-'*10 + ' ')*len(headers))
    for row in reportdata:
        print('%10s %10d %10.2f %10.2f' % row)

レポートプログラムを実行すると、次のような出力が得られるはずです。

>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84

演習4.5: 拡張可能性の問題

print_report() 関数を変更して、平文、HTML、CSV、またはXMLなど、さまざまな異なる出力形式をサポートするようにしたいとします。これを行うために、すべてを行う巨大な関数を書こうとするかもしれません。しかし、そうするとおそらく保守不能な混乱につながります。代わりに、これは継承を使用する完璧な機会です。

始めに、表を作成する際に関係する手順に焦点を当てましょう。表の上部には表の見出しのセットがあります。その後、表データの行が表示されます。これらの手順を取り出して独自のクラスに入れましょう。tableformat.py という名前のファイルを作成し、次のクラスを定義します。

## tableformat.py

class TableFormatter:
    def headings(self, headers):
        '''
        表の見出しを出力します。
        '''
        raise NotImplementedError()

    def row(self, rowdata):
        '''
        表データの1行を出力します。
        '''
        raise NotImplementedError()

このクラスは何もしませんが、すぐに定義される追加のクラスの一種の設計仕様として機能します。このようなクラスは、時には「抽象基底クラス」と呼ばれます。

print_report() 関数を変更して、TableFormatter オブジェクトを入力として受け取り、出力を生成するためにそのメソッドを呼び出すようにします。たとえば、次のようになります。

## report.py
...

def print_report(reportdata, formatter):
    '''
    (名前, 株数, 価格, 変動) のタプルのリストから、見やすくフォーマットされたテーブルを表示します。
    '''
    formatter.headings(['Name','Shares','Price','Change'])
    for name, shares, price, change in reportdata:
        rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
        formatter.row(rowdata)

print_report() に引数を追加したので、portfolio_report() 関数も変更する必要があります。それを次のように変更して、TableFormatter を作成します。

## report.py

import tableformat

...
def portfolio_report(portfoliofile, pricefile):
    '''
    ポートフォリオと価格データファイルを元に株式レポートを作成します。
    '''
    ## データファイルを読み込む
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## レポートデータを作成する
    report = make_report_data(portfolio, prices)

    ## それを出力する
    formatter = tableformat.TableFormatter()
    print_report(report, formatter)

この新しいコードを実行します。

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
... クラッシュ...

NotImplementedError 例外ですぐにクラッシュするはずです。それはあまり面白くありませんが、まさに私たちが期待したものです。次のパートに進みましょう。

演習4.6: 継承を使って異なる出力を生成する

(a) で定義した TableFormatter クラスは、継承を通じて拡張することを目的としています。実際、それが全体のアイデアです。例として、次のように TextTableFormatter クラスを定義します。

## tableformat.py
...
class TextTableFormatter(TableFormatter):
    '''
    平文形式で表を出力します
    '''
    def headings(self, headers):
        for h in headers:
            print(f'{h:>10s}', end=' ')
        print()
        print(('-'*10 + ' ')*len(headers))

    def row(self, rowdata):
        for d in rowdata:
            print(f'{d:>10s}', end=' ')
        print()

portfolio_report() 関数を次のように変更して試してみましょう。

## report.py
...
def portfolio_report(portfoliofile, pricefile):
    '''
    ポートフォリオと価格データファイルを元に株式レポートを作成します。
    '''
    ## データファイルを読み込む
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## レポートデータを作成する
    report = make_report_data(portfolio, prices)

    ## それを出力する
    formatter = tableformat.TextTableFormatter()
    print_report(report, formatter)

これは以前と同じ出力を生成するはずです。

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

しかし、出力を別のものに変更しましょう。CSV形式で出力を生成する新しいクラス CSVTableFormatter を定義します。

## tableformat.py
...
class CSVTableFormatter(TableFormatter):
    '''
    ポートフォリオデータをCSV形式で出力します。
    '''
    def headings(self, headers):
        print(','.join(headers))

    def row(self, rowdata):
        print(','.join(rowdata))

メインプログラムを次のように変更します。

def portfolio_report(portfoliofile, pricefile):
    '''
    ポートフォリオと価格データファイルを元に株式レポートを作成します。
    '''
    ## データファイルを読み込む
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## レポートデータを作成する
    report = make_report_data(portfolio, prices)

    ## それを出力する
    formatter = tableformat.CSVTableFormatter()
    print_report(report, formatter)

これで、次のようなCSV出力が表示されるはずです。

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84

同じ考え方を使って、次の出力を持つ表を生成する HTMLTableFormatter クラスを定義します。

<tr><th>Name</th><th>Shares</th><th>Price</th><th>Change</th></tr>
<tr><td>AA</td><td>100</td><td>9.22</td><td>-22.98</td></tr>
<tr><td>IBM</td><td>50</td><td>106.28</td><td>15.18</td></tr>
<tr><td>CAT</td><td>150</td><td>35.46</td><td>-47.98</td></tr>
<tr><td>MSFT</td><td>200</td><td>20.89</td><td>-30.34</td></tr>
<tr><td>GE</td><td>95</td><td>13.48</td><td>-26.89</td></tr>
<tr><td>MSFT</td><td>50</td><td>20.89</td><td>-44.21</td></tr>
<tr><td>IBM</td><td>100</td><td>106.28</td><td>35.84</td></tr>

メインプログラムを変更して、CSVTableFormatter オブジェクトの代わりに HTMLTableFormatter オブジェクトを作成することでコードをテストします。

✨ 解答を確認して練習

演習4.7: 動的なポリモーフィズム

オブジェクト指向プログラミングの主な特徴の1つは、オブジェクトをプログラムに挿入すると、既存のコードを変更することなく動作するということです。たとえば、TableFormatter オブジェクトを使用することが期待されるプログラムを書いた場合、実際に与える TableFormatter の種類に関係なく動作します。この動作は、時には「ポリモーフィズム」と呼ばれます。

1つの潜在的な問題は、ユーザーが望むフォーマッタを選ぶ方法を考え出すことです。TextTableFormatter のようなクラス名を直接使用するのは、多くの場合面倒です。したがって、いくつかの簡略化されたアプローチを検討するかもしれません。たとえば、次のように if 文をコードに埋め込みます。

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    ポートフォリオと価格データファイルを元に株式レポートを作成します。
    '''
    ## データファイルを読み込む
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## レポートデータを作成する
    report = make_report_data(portfolio, prices)

    ## それを出力する
    if fmt == 'txt':
        formatter = tableformat.TextTableFormatter()
    elif fmt == 'csv':
        formatter = tableformat.CSVTableFormatter()
    elif fmt == 'html':
        formatter = tableformat.HTMLTableFormatter()
    else:
        raise RuntimeError(f'Unknown format {fmt}')
    print_report(report, formatter)

このコードでは、ユーザーが 'txt''csv' のような簡略化された名前を指定して形式を選びます。しかし、そのように portfolio_report() 関数に大きな if 文を入れるのは最善の考えでしょうか。そのコードを他の場所の汎用関数に移す方が良いかもしれません。

tableformat.py ファイルに、'txt''csv'、または 'html' のような出力名を与えると、ユーザーがフォーマッタを作成できる関数 create_formatter(name) を追加します。portfolio_report() を次のように変更します。

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    ポートフォリオと価格データファイルを元に株式レポートを作成します。
    '''
    ## データファイルを読み込む
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## レポートデータを作成する
    report = make_report_data(portfolio, prices)

    ## それを出力する
    formatter = tableformat.create_formatter(fmt)
    print_report(report, formatter)

異なる形式で関数を呼び出して、それが機能していることを確認してみましょう。

✨ 解答を確認して練習

演習4.8: すべてをまとめる

report.py プログラムを変更して、portfolio_report() 関数に出力形式を指定するオプション引数を追加します。たとえば:

>>> report.portfolio_report('portfolio.csv', 'prices.csv', 'txt')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

メインプログラムを変更して、コマンドラインで形式を指定できるようにします。

$ python3 report.py portfolio.csv prices.csv csv
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
$
✨ 解答を確認して練習

考察

拡張可能なコードを書くことは、ライブラリやフレームワークにおける継承の最も一般的な用途の1つです。たとえば、フレームワークは、提供された基底クラスから継承する独自のオブジェクトを定義するよう指示する場合があります。その後、さまざまな機能を実装するさまざまなメソッドを埋めるよう指示されます。

もう少し深い概念は、「自分自身の抽象化を持つ」という考えです。演習では、表をフォーマットするために 自分自身のクラス を定義しました。あなたは自分のコードを見て、「私は代わりに既に誰かが作ったフォーマットライブラリや何かを使うべきだ」と自分自身に言うかもしれません。いいえ、あなたは自分のクラスとライブラリの両方を使うべきです。自分自身のクラスを使うことは、結合の緩和を促進し、より柔軟です。アプリケーションが自分のクラスのプログラミングインターフェイスを使用する限り、内部実装を好きなように変更しても構いません。完全に自作のコードを書くことができます。第三者のパッケージを使うことができます。より良いものを見つけたときに、異なるパッケージに置き換えることができます。問題はありません。インターフェイスを維持していれば、アプリケーションコードは一切壊れません。それは強力な考えであり、このようなことに対して継承を検討する理由の1つです。

とはいえ、オブジェクト指向プログラムを設計することは非常に難しい場合があります。詳細については、おそらくデザインパターンのトピックに関する本を探す必要があります(ただし、この演習で何が起こったかを理解することは、実用的な方法でオブジェクトを使用する点でかなり役立ちます)。

まとめ

おめでとうございます! あなたは継承の実験を完了しました。あなたの技術を向上させるために、LabExでさらに多くの実験を行うことができます。