原文:『実用的なPythonプログラミング』04_02_Inheritance


目次|前節(4.1類)|次節(4.3特殊方法)
4.2継承
継承(inheritance)は拡張可能なプログラムを記述する一般的な手段である.本節では継承の思想(idea)について検討する.
概要
継承:既存のオブジェクトを特殊化します.
class Parent:
    ...

class Child(Parent):
    ...

新クラスChild派生クラス(derived class)またはサブクラス(subclass)と呼ぶ.クラス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
    

    Stockクラスの任意の部分を継承することで変更できます.
    新しいメソッドの追加
    class MyStock(Stock):
        def panic(self):
            self.sell(self.shares)
    

    (注:「panic」はここで「panic selling」を表し、パニック投げ売り)
    使用例:
    >>> 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
    >>>
    

    新しいcost()メソッドは古いcost()メソッドに代わった.他の方法は影響を受けません.
    メソッドオーバーライド
    クラスは、既存のメソッドを拡張したいと同時に、新しい定義で既存の実装を使用したい場合があります.このため、super()関数を用いて実現することができる(注: 時には ):
    class Stock:
        ...
        def cost(self):
            return self.shares * self.price
        ...
    
    class MyStock(Stock):
        def cost(self):
            # Check the call to `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):
            # Check the call to `super` and `__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):
            ...
            # Custom processing
    

    ベースクラスには汎用コードが含まれています.あなたのクラスはベースクラスを継承し、特殊な部分をカスタマイズします.
    「is a」関係
    継承はタイプ関係を確立します.
    class Shape:
        ...
    
    class Circle(Shape):
        ...
    

    オブジェクトのインスタンスを確認します.
    >>> c = Circle(4.0)
    >>> isinstance(c, Shape)
    True
    >>>
    

    重要なヒント:親インスタンスを使用して正常に動作するコードでも、子クラスのインスタンスを使用して正常に動作するのが理想的です.objectベースクラス
    クラスに親がいない場合は、objectベースクラスとして使用されている場合があります.
    class Shape(object):
        ...
    

    Pythonでは、objectすべてのオブジェクトのベースクラスです.
    注意:技術的には必要ではありませんが、通常はobjectPython 2に保存されています.省略すると、クラスは暗黙的にobjectから継承されます.
    多重継承
    クラス定義で複数のベースクラスを指定することで、多重継承を実現できます.
    class Mother:
        ...
    
    class Father:
        ...
    
    class Child(Mother, Father):
        ...
    
    Childクラスは2つの親(Mother,Father)の特性を継承している.ここにはかなり厄介な細部があります.あなたが何をしているか知っていない限り、そうしないでください.詳細は次のセクションで説明しますが、このコースでは多重継承はさらに使用されません.
    練習する
    継承の主な目的は、特にライブラリまたはフレームワークで、拡張性とカスタマイズ性のあるコードをさまざまな方法で作成することです.この点を説明するには、report.pyプログラム中のprint_report()関数を考慮してください.次のように見えます.
    def print_report(reportdata):
        '''
        Print a nicely formated table from a list of (name, shares, price, change) tuples.
        '''
        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)
    
    report.pyプログラムを実行すると、次のような出力が得られるはずです.
    >>> import report
    >>> report.portfolio_report('Data/portfolio.csv', 'Data/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:拡張性の問題
    純テキスト、HTML、CSV、XMLなど、さまざまな出力フォーマットをサポートするために、print_report()関数を変更したいとします.そのため、各機能を実現するために膨大な関数を作成してみることができます.しかし、これはコードが非常に混乱し、メンテナンスができない可能性があります.継承を使用するには絶好の機会です.
    まず、テーブルの作成に関する手順に注目してください.表の上部にタイトルがあります.タイトルの後ろにデータ行があります.これらの手順を使用して、それぞれのクラスに配置しましょう.tableformat.pyという名前のファイルを作成し、次のクラスを定義します.
    # tableformat.py
    
    class TableFormatter:
        def headings(self, headers):
            '''
            Emit the table headings.
            '''
    	raise NotImplementedError()
    
        def row(self, rowdata):
            '''
            Emit a single row of table data.
            '''
    	raise NotImplementedError()
    

    後で他のクラスを定義する設計仕様として使用される以外は、このクラスは何もしません.このようなクラスを「抽象ベースクラス」と呼ぶことがある.print_report()関数を1つTableFormatterオブジェクトを入力として受け入れるように修正し、TableFormatterのメソッドを実行して出力を生成してください.例:
    # report.py
    ...
    
    def print_report(reportdata, formatter):
        '''
        Print a nicely formated table from a list of (name, shares, price, change) tuples.
        '''
        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)
    
    portfolio_report()関数にパラメータを追加したので、portfolio_report()関数も修正する必要があります.portfolio_report()関数を修正して、以下のように作成してくださいTableFormatter:
    # report.py
    
    import tableformat
    
    ...
    def portfolio_report(portfoliofile, pricefile):
        '''
        Make a stock report given portfolio and price data files.
        '''
        # Read data files
        portfolio = read_portfolio(portfoliofile)
        prices = read_prices(pricefile)
    
        # Create the report data
        report = make_report_data(portfolio, prices)
    
        # Print it out
        formatter = tableformat.TableFormatter()
        print_report(report, formatter)
    

    新しいコードを実行します.
    >>> ================================ RESTART ================================
    >>> import report
    >>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
    ... crashes ...
    

    プログラムはすぐにクラッシュするはずで、1つNotImplementedError異常が付いています.これはそれほど興奮していませんが、結果は確かに私たちが期待しています.次のセクションに進みます.
    練習4.6:継承を使用して異なる出力を生成
    a部で定義されているTableFormatterクラスは継承によって拡張されることを目的としている.実際、これが思想全体です.この点を説明するには、以下のように定義してくださいTextTableFormatterクラス:
    # tableformat.py
    ...
    class TextTableFormatter(TableFormatter):
        '''
        Emit a table in plain-text format
        '''
        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):
        '''
        Make a stock report given portfolio and price data files.
        '''
        # Read data files
        portfolio = read_portfolio(portfoliofile)
        prices = read_prices(pricefile)
    
        # Create the report data
        report = make_report_data(portfolio, prices)
    
        # Print it out
        formatter = tableformat.TextTableFormatter()
        print_report(report, formatter)
    

    これにより、以前と同じ出力が生成されるはずです.
    >>> ================================ RESTART ================================
    >>> import report
    >>> report.portfolio_report('Data/portfolio.csv', 'Data/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):
        '''
        Output portfolio data in CSV format.
        '''
        def headings(self, headers):
            print(','.join(headers))
    
        def row(self, rowdata):
            print(','.join(rowdata))
    

    以下のようにメインプログラムを変更してください.
    def portfolio_report(portfoliofile, pricefile):
        '''
        Make a stock report given portfolio and price data files.
        '''
        # Read data files
        portfolio = read_portfolio(portfoliofile)
        prices = read_prices(pricefile)
    
        # Create the report data
        report = make_report_data(portfolio, prices)
    
        # Print it out
        formatter = tableformat.CSVTableFormatter()
        print_report(report, formatter)
    

    次のようなCSV出力が表示されます.
    >>> ================================ RESTART ================================
    >>> import report
    >>> report.portfolio_report('Data/portfolio.csv', 'Data/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クラスを定義し、以下の出力を持つテーブルを生成する.
    NameSharesPriceChange
    AA1009.22-22.98
    IBM50106.2815.18
    CAT15035.46-47.98
    MSFT20020.89-30.34
    GE9513.48-26.89
    MSFT5020.89-44.21
    IBM100106.2835.84
    

    メインプログラムを変更してコードをテストしてください.メインプログラムは、HTMLTableFormatterオブジェクトではなく、CSVTableFormatterオブジェクトを作成します.
    練習4.7:多態
    オブジェクト向けプログラミング(oop)の主な特性は、オブジェクトをプログラムに挿入し、既存のコードを変更することなく実行できることです.たとえば、TableFormatterオブジェクトを使用する予定のプログラムを作成すると、どんなタイプのTableFormatterを与えても正常に動作します.このような行為を「多態」と呼ぶことがある.
    1つの潜在的な問題は、ユーザーが望むフォーマットを選択する方法を明らかにすることです.TextTableFormatterのように直接類名を使うのは、通常ちょっと煩わしいです.そのため、簡略化された方法を考えるべきです.コードに埋め込むことができますif文:
    def portfolio_report(portfoliofile, pricefile, fmt='txt'):
        '''
        Make a stock report given portfolio and price data files.
        '''
        # Read data files
        portfolio = read_portfolio(portfoliofile)
        prices = read_prices(pricefile)
    
        # Create the report data
        report = make_report_data(portfolio, prices)
    
        # Print it out
        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ファイルには、create_formatter(name)という名前の関数を追加してください.この関数により、ユーザが所定の出力名(例えば'txt''csv'、または'html')のフォーマット(formatter)を作成できるようにします.下記のように修正してくださいportfolio_report()関数:
    def portfolio_report(portfoliofile, pricefile, fmt='txt'):
        '''
        Make a stock report given portfolio and price data files.
        '''
        # Read data files
        portfolio = read_portfolio(portfoliofile)
        prices = read_prices(pricefile)
    
        # Create the report data
        report = make_report_data(portfolio, prices)
    
        # Print it out
        formatter = tableformat.create_formatter(fmt)
        print_report(report, formatter)
    

    異なるフォーマットで関数を呼び出して、正常に動作していることを確認します.
    練習4.8:まとめreport.pyプログラムを修正して、portfolio_report()関数がオプションパラメータで出力フォーマットを指定できるようにしてください.例:
    >>> report.portfolio_report('Data/portfolio.csv', 'Data/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
    >>>
    

    コマンドラインで出力フォーマットを指定できるように、メインプログラムを変更してください.
    bash $ python3 report.py Data/portfolio.csv Data/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
    bash $
    

    ディスカッション
    ライブラリとフレームワークでは、拡張可能なプログラムを作成することが継承の最も一般的な用途の1つです.たとえば、フレームワークでは、指定したベースクラスを継承する独自のオブジェクトを定義します.そして、さまざまな機能を実現する関数を追加することができます.
    もう一つのより深い概念は「抽象的な思想を持つ」ことだ.練習では、テーブルをフォーマットするための独自のクラスを定義します.自分のコードを見て、「フォーマットライブラリや他の人が書いたものだけを使うべきだ!」と自分に伝えるかもしれません.いいえ、自分のクラスとライブラリを同時に使用する必要があります.独自のクラスを使用すると、プログラムの結合性を低下させ、プログラムの柔軟性を高めることができます.プログラムが使用するアプリケーションインタフェースが自分で定義したクラスから来ている限り、プログラムの内部実装を変更して、あなたが考えているように動作させることができます.フルカスタマイズ(all-custom)コードを作成したり、サードパーティ製パッケージを使用したりすることができます.より良いパッケージを見つけたら、サードパーティ製のパッケージを別のパッケージに置き換えることができます.これは重要ではありません.このインタフェースを保持すれば、アプリケーションコードは中断されません.これは強力な思想であり、継承を使用すべき理由の一つでもある.
    すなわち,オブジェクト向けのプログラムを設計することは非常に困難である可能性がある.詳細については、デザインモデルのテーマに関する本を探して読むべきかもしれません(この練習の内容を理解することは、実用的な方法で使用対象から遠く離れていますが).
    目次|前節(4.1類)|次節(4.3特殊方法)
    注:完全な翻訳を参照https://github.com/codists/practical-python-zh