はじめに
この実験では、Python の高階関数について学びます。高階関数は、他の関数を引数として受け取ったり、関数を結果として返したりすることができます。この概念は関数型プログラミングにおいて重要であり、よりモジュール化され再利用可能なコードを書くことができます。
高階関数が何であるかを理解し、関数を引数として受け取る高階関数を作成し、既存の関数を高階関数を使用するようにリファクタリングし、Python の組み込み関数 map() を利用します。実験中に reader.py ファイルを変更します。
コードの重複を理解する
まず、reader.py ファイルの現在のコードを見てみましょう。プログラミングでは、既存のコードを調べることは、仕組みを理解し、改善すべき箇所を特定するための重要なステップです。WebIDE で reader.py ファイルを開くことができます。これには 2 つの方法があります。ファイルエクスプローラーでファイルをクリックするか、ターミナルで以下のコマンドを実行することができます。これらのコマンドは、まずプロジェクトディレクトリに移動し、次に reader.py ファイルの内容を表示します。
cd ~/project
cat reader.py
コードを見ると、2 つの関数があることに気づくでしょう。Python の関数は、特定のタスクを実行するコードブロックです。以下は 2 つの関数とその機能です。
csv_as_dicts(): この関数は CSV データを受け取り、辞書のリストに変換します。Python の辞書はキーと値のペアのコレクションであり、データを構造化して保存するのに便利です。csv_as_instances(): この関数は CSV データを受け取り、インスタンスのリストに変換します。インスタンスはクラスから作成されたオブジェクトであり、クラスはオブジェクトを作成するためのブループリントです。
では、これら 2 つの関数をもう少し詳しく見てみましょう。これらの関数は非常に似ていることがわかります。両方の関数は以下の手順に従います。
- まず、空の
recordsリストを初期化します。Python のリストは、異なる型のアイテムのコレクションです。空のリストを初期化するとは、アイテムが含まれていないリストを作成することであり、処理されたデータを格納するために使用されます。 - 次に、
csv.reader()を使用して入力を解析します。解析とは、入力データを分析して意味のある情報を抽出することです。この場合、csv.reader()は CSV データを 1 行ずつ読み取るのに役立ちます。 - ヘッダーの処理方法は同じです。CSV ファイルのヘッダーは通常、列名を含む最初の行です。
- その後、CSV データの各行をループ処理します。ループは、コードブロックを複数回実行できるプログラミング構造です。
- 各行について、レコードを作成するために処理します。このレコードは、関数に応じて辞書またはインスタンスのいずれかになります。
- レコードを
recordsリストに追加します。追加とは、リストの末尾にアイテムを追加することです。 - 最後に、処理されたすべてのデータを含む
recordsリストを返します。
このコードの重複はいくつかの理由で問題となります。コードが重複すると、
- メンテナンスが難しくなります。コードに変更を加える必要がある場合、複数の場所で同じ変更を加える必要があります。これにはより多くの時間と労力がかかります。
- 変更は複数の場所で実装する必要があります。これにより、ある場所で変更を忘れる可能性が高くなり、動作が不一致になる原因となります。
- バグが発生する可能性も高くなります。バグは、コードが予期せぬ動作をする原因となるエラーです。
これら 2 つの関数の唯一の本質的な違いは、行をレコードに変換する方法です。これは、高階関数が非常に役立つ典型的なケースです。高階関数は、他の関数を引数として受け取ったり、関数を結果として返したりする関数です。
これらの関数の動作をよりよく理解するために、いくつかのサンプル使用例を見てみましょう。以下のコードは、csv_as_dicts() と csv_as_instances() の使用方法を示しています。
## Example of using csv_as_dicts
with open('portfolio.csv') as f:
portfolio = csv_as_dicts(f, [str, int, float])
print(portfolio[0]) ## {'name': 'AA', 'shares': 100, 'price': 32.2}
## Example of using csv_as_instances
class Stock:
@classmethod
def from_row(cls, row):
return cls(row[0], int(row[1]), float(row[2]))
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
with open('portfolio.csv') as f:
portfolio = csv_as_instances(f, Stock)
print(portfolio[0].name, portfolio[0].shares, portfolio[0].price) ## AA 100 32.2
次のステップでは、このコードの重複を解消するための高階関数を作成します。これにより、コードがよりメンテナンスしやすく、エラーが発生しにくくなります。
高階関数の作成
Python では、高階関数とは、他の関数を引数として受け取ることができる関数です。これにより、より高い柔軟性とコードの再利用性が実現されます。では、convert_csv() という高階関数を作成しましょう。この関数は、CSV データを処理する共通の操作を担当し、CSV の各行をレコードに変換する方法をカスタマイズできるようにします。
WebIDE で reader.py ファイルを開きます。CSV データのイテラブル、変換関数、およびオプションとして列ヘッダーを受け取る関数を追加します。変換関数は、CSV の各行をレコードに変換するために使用されます。
以下は convert_csv() 関数のコードです。これを reader.py ファイルにコピーして貼り付けてください。
def convert_csv(lines, conversion_func, *, headers=None):
'''
Convert lines of CSV data using the provided conversion function
Args:
lines: An iterable containing CSV data
conversion_func: A function that takes headers and a row and returns a record
headers: Column headers (optional). If None, the first row is used as headers
Returns:
A list of records as processed by conversion_func
'''
records = []
rows = csv.reader(lines)
if headers is None:
headers = next(rows)
for row in rows:
record = conversion_func(headers, row)
records.append(record)
return records
この関数が何をするかを分解してみましょう。まず、変換されたレコードを格納するために records という空のリストを初期化します。次に、csv.reader() 関数を使用して CSV データの行を読み取ります。ヘッダーが提供されていない場合、最初の行をヘッダーとして使用します。それ以降の各行について、conversion_func を適用して行をレコードに変換し、records リストに追加します。最後に、レコードのリストを返します。
では、convert_csv() 関数をテストするための簡単な変換関数が必要です。この関数は、ヘッダーと行を受け取り、ヘッダーをキーとして行を辞書に変換します。
以下は make_dict() 関数のコードです。この関数も reader.py ファイルに追加してください。
def make_dict(headers, row):
'''
Convert a row to a dictionary using the provided headers
'''
return dict(zip(headers, row))
make_dict() 関数は、zip() 関数を使用して各ヘッダーを行内の対応する値とペアにし、それらのペアから辞書を作成します。
これらの関数をテストしましょう。ターミナルで以下のコマンドを実行して Python シェルを開きます。
cd ~/project
python3 -i reader.py
python3 コマンドの -i オプションは、Python インタープリターを対話モードで起動し、reader.py ファイルをインポートするので、先ほど定義した関数を使用することができます。
Python シェルで、以下のコードを実行して関数をテストします。
## Open the CSV file
lines = open('portfolio.csv')
## Convert to a list of dictionaries using our new function
result = convert_csv(lines, make_dict)
## Print the result
print(result)
このコードは portfolio.csv ファイルを開き、convert_csv() 関数と make_dict() 変換関数を使用して CSV データを辞書のリストに変換し、結果を出力します。
以下のような出力が表示されるはずです。
[{'name': 'AA', 'shares': '100', 'price': '32.20'}, {'name': 'IBM', 'shares': '50', 'price': '91.10'}, {'name': 'CAT', 'shares': '150', 'price': '83.44'}, {'name': 'MSFT', 'shares': '200', 'price': '51.23'}, {'name': 'GE', 'shares': '95', 'price': '40.37'}, {'name': 'MSFT', 'shares': '50', 'price': '65.10'}, {'name': 'IBM', 'shares': '100', 'price': '70.44'}]
この出力は、高階関数 convert_csv() が正しく動作していることを示しています。他の関数を引数として受け取る関数を成功裏に作成し、CSV データの変換方法を簡単に変更できるようになりました。
Python シェルを終了するには、exit() と入力するか、Ctrl+D を押します。
既存の関数をリファクタリングする
ここでは、convert_csv() という名前の高階関数を作成しました。高階関数とは、他の関数を引数として受け取ったり、関数を結果として返したりする関数です。これは Python で非常に強力な概念であり、よりモジュール化され再利用可能なコードを書くのに役立ちます。このセクションでは、この高階関数を使って、元の関数 csv_as_dicts() と csv_as_instances() をリファクタリングします。リファクタリングとは、既存のコードの外部的な振る舞いを変えることなく、内部構造を改善するためにコードを再構築するプロセスで、例えばコードの重複を排除することが目的です。
まず、WebIDE で reader.py ファイルを開きましょう。以下のように関数を更新します。
- まず、
csv_as_dicts()関数を置き換えます。この関数は、CSV データの行を辞書のリストに変換するために使用されます。以下が新しいコードです。
def csv_as_dicts(lines, types, *, headers=None):
'''
Convert lines of CSV data into a list of dictionaries
'''
def dict_converter(headers, row):
return {name: func(val) for name, func, val in zip(headers, types, row)}
return convert_csv(lines, dict_converter, headers=headers)
このコードでは、headers と row を引数とする内部関数 dict_converter を定義しています。この関数は辞書内包表記を使って、キーがヘッダー名で、値が行内の値に対応する型変換関数を適用した結果である辞書を作成します。そして、dict_converter 関数を引数として convert_csv() 関数を呼び出します。
- 次に、
csv_as_instances()関数を置き換えます。この関数は、CSV データの行を指定されたクラスのインスタンスのリストに変換するために使用されます。以下が新しいコードです。
def csv_as_instances(lines, cls, *, headers=None):
'''
Convert lines of CSV data into a list of instances
'''
def instance_converter(headers, row):
return cls.from_row(row)
return convert_csv(lines, instance_converter, headers=headers)
このコードでは、headers と row を引数とする内部関数 instance_converter を定義しています。この関数は、指定されたクラス cls の from_row クラスメソッドを呼び出して、行からインスタンスを作成します。そして、instance_converter 関数を引数として convert_csv() 関数を呼び出します。
これらの関数をリファクタリングした後、期待通りに動作することを確認するためにテストする必要があります。これを行うには、Python シェルで以下のコマンドを実行します。
cd ~/project
python3 -i reader.py
cd ~/project コマンドは、現在の作業ディレクトリを project ディレクトリに変更します。python3 -i reader.py コマンドは、reader.py ファイルを対話モードで実行します。これは、ファイルの実行が終了した後も Python コードを続けて実行できることを意味します。
Python シェルが開いたら、以下のコードを実行してリファクタリングした関数をテストします。
## Define a simple Stock class for testing
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@classmethod
def from_row(cls, row):
return cls(row[0], int(row[1]), float(row[2]))
def __repr__(self):
return f'Stock({self.name}, {self.shares}, {self.price})'
## Test csv_as_dicts
with open('portfolio.csv') as f:
portfolio_dicts = csv_as_dicts(f, [str, int, float])
print("First dictionary:", portfolio_dicts[0])
## Test csv_as_instances
with open('portfolio.csv') as f:
portfolio_instances = csv_as_instances(f, Stock)
print("First instance:", portfolio_instances[0])
このコードでは、まずテスト用に簡単な Stock クラスを定義しています。__init__ メソッドは Stock インスタンスの属性を初期化します。from_row クラスメソッドは、CSV データの行から Stock インスタンスを作成します。__repr__ メソッドは Stock インスタンスの文字列表現を提供します。
次に、portfolio.csv ファイルを開き、型変換関数のリストとともに csv_as_dicts() 関数に渡すことで、この関数をテストします。結果のリストの最初の辞書を出力します。
最後に、portfolio.csv ファイルを開き、Stock クラスとともに csv_as_instances() 関数に渡すことで、この関数をテストします。結果のリストの最初のインスタンスを出力します。
すべてが正しく動作していれば、以下のような出力が表示されるはずです。
First dictionary: {'name': 'AA', 'shares': 100, 'price': 32.2}
First instance: Stock(AA, 100, 32.2)
この出力は、リファクタリングした関数が正しく動作していることを示しています。同じ機能を維持しながら、コードの重複を成功裏に排除しました。
Python シェルを終了するには、exit() と入力するか、Ctrl+D を押します。
map() 関数の使用
Python では、高階関数とは、他の関数を引数として受け取ったり、関数を結果として返したりする関数です。Python の map() 関数は高階関数の良い例です。これは、リストやタプルなどのイテラブルの各要素に対して指定された関数を適用する強力なツールです。各要素に関数を適用した後、結果のイテレータを返します。この機能により、map() は CSV ファイルの行のようなデータ列の処理に最適です。
map() 関数の基本的な構文は以下の通りです。
map(function, iterable, ...)
ここで、function は iterable の各要素に対して実行したい操作です。iterable はリストやタプルのような要素の列です。
簡単な例を見てみましょう。数値のリストがあり、そのリスト内の各数値を二乗したいとします。これを達成するために map() 関数を使用できます。以下はその方法です。
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x * x, numbers))
print(squared) ## Output: [1, 4, 9, 16, 25]
この例では、まず numbers というリストを定義しています。次に、map() 関数を使用しています。lambda 関数 lambda x: x * x は、numbers リストの各要素に対して実行したい操作です。map() 関数はこの lambda 関数をリスト内の各数値に適用します。map() はイテレータを返すため、list() 関数を使用してリストに変換しています。最後に、元の数値を二乗した値を含む squared リストを出力します。
では、map() 関数を使って convert_csv() 関数をどのように修正できるか見てみましょう。以前は、CSV データの行を反復処理するために for ループを使用していました。今回は、その for ループを map() 関数に置き換えます。
def convert_csv(lines, conversion_func, *, headers=None):
'''
Convert lines of CSV data using the provided conversion function
'''
rows = csv.reader(lines)
if headers is None:
headers = next(rows)
## Use map to apply conversion_func to each row
records = list(map(lambda row: conversion_func(headers, row), rows))
return records
この更新された convert_csv() 関数は、以前のバージョンとまったく同じことを行いますが、for ループの代わりに map() 関数を使用しています。map() 内の lambda 関数は、CSV データの各行を取得し、ヘッダーとともに conversion_func を適用します。
この更新された関数が正しく動作することを確認するためにテストしましょう。まず、ターミナルを開き、プロジェクトディレクトリに移動します。次に、reader.py ファイルを使って Python 対話型シェルを起動します。
cd ~/project
python3 -i reader.py
Python シェルに入ったら、以下のコードを実行して更新された convert_csv() 関数をテストします。
## Test the updated convert_csv function
with open('portfolio.csv') as f:
result = convert_csv(f, make_dict)
print(result[0]) ## Should print the first dictionary
## Test that csv_as_dicts still works
with open('portfolio.csv') as f:
portfolio = csv_as_dicts(f, [str, int, float])
print(portfolio[0]) ## Should print the first dictionary with converted types
このコードを実行した後、以下のような出力が表示されるはずです。
{'name': 'AA', 'shares': '100', 'price': '32.20'}
{'name': 'AA', 'shares': 100, 'price': 32.2}
この出力は、map() 関数を使用した更新された convert_csv() 関数が正しく動作し、それに依存する関数も期待通りに動作し続けることを示しています。
map() 関数を使用することにはいくつかの利点があります。
forループよりも簡潔に書けます。forループのために複数行のコードを書く代わりに、map()を使って 1 行で同じ結果を得ることができます。- シーケンス内の各要素を変換する意図を明確に伝えます。
map()を見ると、イテラブルの各要素に関数を適用していることがすぐにわかります。 - イテレータを返すため、メモリ効率が良い場合があります。イテレータは値を逐次生成するため、一度にすべての結果をメモリに格納する必要がありません。この例では、
map()が返すイテレータをリストに変換しましたが、場合によってはイテレータを直接使用してメモリを節約することができます。
Python シェルを終了するには、exit() と入力するか、Ctrl+D を押します。
まとめ
この実験では、Python の高階関数について学び、それがよりモジュール化され保守可能なコードを書くのにどのように役立つかを理解しました。まず、2 つの類似した関数におけるコードの重複を特定しました。次に、変換関数を引数として受け取る高階関数 convert_csv() を作成し、元の関数をリファクタリングしてこの関数を使用するようにしました。最後に、高階関数を更新して Python の組み込み関数 map() を利用するようにしました。
これらのテクニックは、Python プログラマのツールキットにおいて強力な武器です。高階関数はコードの再利用と関心事の分離を促進し、関数を引数として渡すことでより柔軟でカスタマイズ可能な動作が可能になります。map() のような関数は、データを変換する簡潔な方法を提供します。これらの概念を習得することで、より簡潔で保守可能でエラーが少ない Python コードを書くことができます。