はじめに
この実験では、Python の型チェックとインターフェースについての理解を深める方法を学びます。テーブル形式のモジュールを拡張することで、抽象基底クラスやインターフェース検証などの概念を実装し、より堅牢で保守しやすいコードを作成します。
この実験は、以前の演習で学んだ概念を基に、型の安全性とインターフェースの設計パターンに焦点を当てています。あなたの目標は、関数パラメータの型チェックの実装、抽象基底クラスを使用したインターフェースの作成と使用、およびテンプレートメソッドパターンの適用によるコードの重複削減です。データをテーブル形式にするモジュール tableformat.py と、CSV ファイルを読み取るモジュール reader.py を修正します。
print_table() への型チェックの追加
このステップでは、tableformat.py ファイル内の print_table() 関数を改善します。formatter パラメータが有効な TableFormatter インスタンスであるかどうかをチェックする機能を追加します。なぜこれが必要なのでしょうか?型チェックは、コードの安全ネットのようなものです。取り扱うデータが正しい型であることを確認し、見つけにくいバグを防ぐのに役立ちます。
Python での型チェックの理解
型チェックは、プログラミングにおいて非常に有用なテクニックです。開発プロセスの早い段階でエラーを検出することができます。Python では、さまざまな型のオブジェクトを扱うことが多く、関数に特定の型のオブジェクトが渡されることを期待することがあります。オブジェクトが特定の型またはそのサブクラスであるかどうかをチェックするには、isinstance() 関数を使用できます。たとえば、リストを期待する関数がある場合、isinstance() を使用して入力が実際にリストであることを確認できます。
print_table() 関数の修正
まず、コードエディタで tableformat.py ファイルを開きます。ファイルの末尾までスクロールすると、print_table() 関数が見つかります。最初は次のようになっています。
def print_table(data, columns, formatter):
'''
Print a table showing selected columns from a data source
using the given formatter.
'''
formatter.headings(columns)
for item in data:
rowdata = [str(getattr(item, col)) for col in columns]
formatter.row(rowdata)
この関数は、いくつかのデータ、列のリスト、およびフォーマッタを受け取ります。そして、フォーマッタを使用してテーブルを印刷します。ただし、現時点ではフォーマッタが正しい型であるかどうかをチェックしていません。
型チェックを追加するために修正しましょう。isinstance() 関数を使用して、formatter パラメータが TableFormatter のインスタンスであるかどうかをチェックします。もしそうでなければ、明確なメッセージを含む TypeError を発生させます。修正後のコードは次のとおりです。
def print_table(data, columns, formatter):
'''
Print a table showing selected columns from a data source
using the given formatter.
'''
if not isinstance(formatter, TableFormatter):
raise TypeError("Expected a TableFormatter")
formatter.headings(columns)
for item in data:
rowdata = [str(getattr(item, col)) for col in columns]
formatter.row(rowdata)
型チェック実装のテスト
型チェックを追加したので、それが機能することを確認する必要があります。test_tableformat.py という新しい Python ファイルを作成しましょう。以下のコードを記述します。
import stock
import reader
import tableformat
## Read portfolio data
portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
## Define a formatter that doesn't inherit from TableFormatter
class MyFormatter:
def headings(self, headers):
pass
def row(self, rowdata):
pass
## Try to use the non-compliant formatter
try:
tableformat.print_table(portfolio, ['name', 'shares', 'price'], MyFormatter())
print("Test failed - type checking not implemented")
except TypeError as e:
print(f"Test passed - caught error: {e}")
このコードでは、まずポートフォリオデータを読み込みます。次に、TableFormatter を継承していない新しいフォーマッタクラス MyFormatter を定義します。この非互換のフォーマッタを print_table() 関数で使用しようとします。型チェックが機能していれば、TypeError が発生するはずです。
テストを実行するには、ターミナルを開き、test_tableformat.py ファイルがあるディレクトリに移動します。次のコマンドを実行します。
python test_tableformat.py
すべてが正しく動作していれば、次のような出力が表示されます。
Test passed - caught error: Expected a TableFormatter
この出力は、型チェックが期待どおりに機能していることを確認します。これで、print_table() 関数は TableFormatter のインスタンスまたはそのサブクラスであるフォーマッタのみを受け入れるようになりました。
抽象基底クラスの実装
このステップでは、Python の abc モジュールを使用して、TableFormatter クラスを適切な抽象基底クラス (ABC) に変換します。まずは、抽象基底クラスとは何か、なぜ必要なのかを理解しましょう。
抽象基底クラスの理解
抽象基底クラスは、Python の特殊なクラスです。直接オブジェクトを作成することができないクラスで、つまりインスタンス化することができません。抽象基底クラスの主な目的は、サブクラスに共通のインターフェースを定義することです。すべてのサブクラスが従わなければならない一連のルールを設定します。具体的には、サブクラスに特定のメソッドを実装することを要求します。
抽象基底クラスに関するいくつかの重要な概念を紹介します。
- Python では
abcモジュールを使用して抽象基底クラスを作成します。 @abstractmethodデコレータでマークされたメソッドはルールのようなものです。抽象基底クラスを継承するすべてのサブクラスは、これらのメソッドを実装しなければなりません。- 抽象基底クラスを継承しているが、すべての必要なメソッドを実装していないクラスのオブジェクトを作成しようとすると、Python はエラーを発生させます。
抽象基底クラスの基本を理解したので、TableFormatter クラスを抽象基底クラスに変更する方法を見てみましょう。
TableFormatter クラスの修正
tableformat.py ファイルを開きます。TableFormatter クラスを変更して、abc モジュールを使用し、抽象基底クラスにします。
- まず、
abcモジュールから必要なものをインポートする必要があります。ファイルの先頭に次のインポート文を追加します。
## tableformat.py
from abc import ABC, abstractmethod
このインポート文は、2 つの重要なものを持ってきます。ABC は Python のすべての抽象基底クラスの基底クラスで、abstractmethod はメソッドを抽象メソッドとしてマークするために使用するデコレータです。
- 次に、
TableFormatterクラスを修正します。抽象基底クラスにするためにABCを継承し、@abstractmethodデコレータを使用してそのメソッドを抽象メソッドとしてマークします。修正後のクラスは次のようになります。
class TableFormatter(ABC):
@abstractmethod
def headings(self, headers):
'''
Emit the table headings.
'''
pass
@abstractmethod
def row(self, rowdata):
'''
Emit a single row of table data.
'''
pass
この修正されたクラスについていくつか注意点があります。
- クラスは現在
ABCを継承しており、これは公式に抽象基底クラスであることを意味します。 headingsメソッドとrowメソッドの両方が@abstractmethodでデコレートされています。これは、TableFormatterのサブクラスがこれらのメソッドを実装しなければならないことを Python に伝えます。NotImplementedErrorをpassに置き換えました。@abstractmethodデコレータがサブクラスがこれらのメソッドを実装することを保証するので、もはやNotImplementedErrorは必要ありません。
抽象基底クラスのテスト
TableFormatter クラスを抽象基底クラスにしたので、正しく動作するかテストしましょう。次のコードを持つ test_abc.py というファイルを作成します。
from tableformat import TableFormatter
## Test case 1: Define a class with a misspelled method
try:
class NewFormatter(TableFormatter):
def headers(self, headings): ## Misspelled 'headings'
pass
def row(self, rowdata):
pass
f = NewFormatter()
print("Test 1 failed - abstract method enforcement not working")
except TypeError as e:
print(f"Test 1 passed - caught error: {e}")
## Test case 2: Define a class that properly implements all methods
try:
class ProperFormatter(TableFormatter):
def headings(self, headers):
pass
def row(self, rowdata):
pass
f = ProperFormatter()
print("Test 2 passed - proper implementation works")
except TypeError as e:
print(f"Test 2 failed - error: {e}")
このコードには 2 つのテストケースがあります。最初のテストケースでは、TableFormatter を継承しようとするが、メソッド名が誤っている NewFormatter クラスを定義しています。2 番目のテストケースでは、すべての必要なメソッドを正しく実装した ProperFormatter クラスを定義しています。
テストを実行するには、ターミナルを開き、次のコマンドを実行します。
python test_abc.py
次のような出力が表示されるはずです。
Test 1 passed - caught error: Can't instantiate abstract class NewFormatter with abstract methods headings
Test 2 passed - proper implementation works
この出力は、抽象基底クラスが期待どおりに動作していることを確認します。最初のテストケースは、NewFormatter クラスが headings メソッドを正しく実装していなかったために失敗します。2 番目のテストケースは、ProperFormatter クラスがすべての必要なメソッドを実装していたために成功します。
アルゴリズムテンプレートクラスの作成
このステップでは、抽象基底クラスを使用してテンプレートメソッドパターンを実装します。目的は、CSV 解析機能におけるコードの重複を削減することです。コードの重複は、コードの保守と更新を困難にする可能性があります。テンプレートメソッドパターンを使用することで、CSV 解析コードの共通構造を作成し、サブクラスに具体的な詳細を処理させることができます。
テンプレートメソッドパターンの理解
テンプレートメソッドパターンは、振る舞いに関するデザインパターンです。アルゴリズムの青写真のようなものです。メソッド内で、アルゴリズムの全体的な構造または「骨格」を定義します。ただし、すべてのステップを完全に実装するわけではありません。代わりに、一部のステップをサブクラスに委譲します。これは、サブクラスがアルゴリズムの特定の部分を再定義できる一方で、その全体構造を変更することなく実現できることを意味します。
私たちのケースでは、reader.py ファイルを見ると、read_csv_as_dicts() 関数と read_csv_as_instances() 関数には多くの類似したコードがあることに気づくでしょう。これらの主な違いは、CSV ファイルの行からレコードを作成する方法です。テンプレートメソッドパターンを使用することで、同じコードを複数回書くことを避けることができます。
CSVParser 基底クラスの追加
まず、CSV 解析用の抽象基底クラスを追加しましょう。reader.py ファイルを開きます。インポート文の直後に、CSVParser 抽象基底クラスをファイルの先頭に追加します。
## reader.py
import csv
from abc import ABC, abstractmethod
class CSVParser(ABC):
def parse(self, filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows)
for row in rows:
record = self.make_record(headers, row)
records.append(record)
return records
@abstractmethod
def make_record(self, headers, row):
pass
この CSVParser クラスは、CSV 解析のテンプレートとして機能します。parse メソッドには、CSV ファイルを読み取るための共通のステップが含まれています。たとえば、ファイルを開く、ヘッダーを取得する、行を反復処理するなどです。行からレコードを作成する具体的なロジックは、make_record() メソッドに抽象化されています。これは抽象メソッドであるため、CSVParser を継承するすべてのクラスはこのメソッドを実装しなければなりません。
具体的なパーサークラスの実装
これで基底クラスができたので、具体的なパーサークラスを作成する必要があります。これらのクラスは、具体的なレコード作成ロジックを実装します。
class DictCSVParser(CSVParser):
def __init__(self, types):
self.types = types
def make_record(self, headers, row):
return { name: func(val) for name, func, val in zip(headers, self.types, row) }
class InstanceCSVParser(CSVParser):
def __init__(self, cls):
self.cls = cls
def make_record(self, headers, row):
return self.cls.from_row(row)
DictCSVParser クラスは、レコードを辞書として作成するために使用されます。コンストラクタで型のリストを受け取ります。make_record メソッドは、これらの型を使用して行内の値を変換し、辞書を作成します。
InstanceCSVParser クラスは、レコードをクラスのインスタンスとして作成するために使用されます。コンストラクタでクラスを受け取ります。make_record メソッドは、そのクラスの from_row メソッドを呼び出して、行からインスタンスを作成します。
元の関数のリファクタリング
では、元の read_csv_as_dicts() 関数と read_csv_as_instances() 関数をリファクタリングして、これらの新しいクラスを使用するようにしましょう。
def read_csv_as_dicts(filename, types):
'''
Read a CSV file into a list of dictionaries with appropriate type conversion.
'''
parser = DictCSVParser(types)
return parser.parse(filename)
def read_csv_as_instances(filename, cls):
'''
Read a CSV file into a list of instances of a class.
'''
parser = InstanceCSVParser(cls)
return parser.parse(filename)
これらのリファクタリングされた関数は、元の関数と同じインターフェースを持っています。ただし、内部的には、先ほど作成した新しいパーサークラスを使用しています。このようにして、共通の CSV 解析ロジックと具体的なレコード作成ロジックを分離しました。
実装のテスト
リファクタリングしたコードが正しく動作するか確認しましょう。test_reader.py という名前のファイルを作成し、以下のコードを追加します。
import reader
import stock
## Test the refactored read_csv_as_instances function
portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
print("First stock:", portfolio[0])
## Test the refactored read_csv_as_dicts function
portfolio_dicts = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("First stock as dict:", portfolio_dicts[0])
## Test direct use of a parser
parser = reader.DictCSVParser([str, int, float])
portfolio_dicts2 = parser.parse('portfolio.csv')
print("First stock from direct parser:", portfolio_dicts2[0])
テストを実行するには、ターミナルを開き、次のコマンドを実行します。
python test_reader.py
次のような出力が表示されるはずです。
First stock: Stock('AA', 100, 32.2)
First stock as dict: {'name': 'AA', 'shares': 100, 'price': 32.2}
First stock from direct parser: {'name': 'AA', 'shares': 100, 'price': 32.2}
この出力が表示されれば、リファクタリングしたコードが正しく動作していることを意味します。元の関数とパーサーの直接使用の両方が、期待される結果を生成しています。
まとめ
この実験では、Python コードを強化するためのいくつかの重要なオブジェクト指向プログラミングの概念を学びました。まず、print_table() 関数で型チェックを実装しました。これにより、有効なフォーマッターのみが使用されることが保証され、コードの堅牢性が向上します。次に、TableFormatter クラスを抽象基底クラスに変換し、サブクラスに特定のメソッドを実装することを強制しました。
さらに、CSVParser 抽象基底クラスとその具体的な実装を作成することで、テンプレートメソッドパターンを適用しました。これにより、一貫したアルゴリズム構造を維持しながら、コードの重複を削減します。これらの技術は、特に大規模なアプリケーションにおいて、より保守可能で堅牢な Python コードを作成するために重要です。さらなる学習のために、Python の型ヒント (PEP 484)、プロトコルクラス、および Python のデザインパターンを探索してみてください。