はじめに
この実験では、Python の関数とメソッドの基本的な側面を探索する方法を学びます。また、パラメータを効果的に設計することで、関数をより柔軟にすることができます。
さらに、コードの可読性と安全性を高めるために型ヒントを実装します。これは、高品質な Python コードを書くために重要です。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
この実験では、Python の関数とメソッドの基本的な側面を探索する方法を学びます。また、パラメータを効果的に設計することで、関数をより柔軟にすることができます。
さらに、コードの可読性と安全性を高めるために型ヒントを実装します。これは、高品質な Python コードを書くために重要です。
前の演習では、CSV ファイルを読み込み、データを様々なデータ構造に格納するコードに遭遇したことがあるかもしれません。このコードの目的は、CSV ファイルから生のテキストデータを取得し、辞書やクラスインスタンスなどのより有用な Python オブジェクトに変換することです。この変換は、Python プログラム内でデータをより構造化された意味のある方法で扱うことができるため、不可欠です。
CSV ファイルを読み込む典型的なパターンは、特定の構造に従うことが多いです。以下は、CSV ファイルを読み込み、各行を辞書に変換する関数の例です。
import csv
def read_csv_as_dicts(filename, types):
records = []
with open(filename) as file:
rows = csv.reader(file)
headers = next(rows)
for row in rows:
record = { name: func(val)
for name, func, val in zip(headers, types, row) }
records.append(record)
return records
この関数がどのように動作するかを分解してみましょう。まず、Python で CSV ファイルを扱う機能を提供する csv
モジュールをインポートします。この関数は 2 つのパラメータを受け取ります。filename
は読み込む CSV ファイルの名前で、types
は各列のデータを適切なデータ型に変換するために使用される関数のリストです。
関数の内部では、CSV ファイルの各行を表す辞書を格納するために、records
という空のリストを初期化します。その後、with
文を使用してファイルを開きます。これにより、コードブロックの実行後にファイルが適切に閉じられることが保証されます。csv.reader
関数を使用して、CSV ファイルの各行を読み取るイテレータを作成します。最初の行はヘッダーと見なされるため、next
関数を使用して取得します。
次に、関数は CSV ファイルの残りの行を反復処理します。各行について、辞書内包表記を使用して辞書を作成します。辞書のキーは列ヘッダーで、値は types
リストからの対応する型変換関数を行内の値に適用した結果です。最後に、辞書は records
リストに追加され、関数は辞書のリストを返します。
では、CSV ファイルからのデータをクラスインスタンスに読み込む類似の関数を見てみましょう。
def read_csv_as_instances(filename, cls):
records = []
with open(filename) as file:
rows = csv.reader(file)
headers = next(rows)
for row in rows:
record = cls.from_row(row)
records.append(record)
return records
この関数は前の関数と似ていますが、辞書を作成する代わりに、クラスのインスタンスを作成します。この関数は 2 つのパラメータを受け取ります。filename
は読み込む CSV ファイルの名前で、cls
はインスタンスが作成されるクラスです。
関数の内部では、前の関数と同様の構造に従います。クラスインスタンスを格納するために、records
という空のリストを初期化します。その後、ファイルを開き、ヘッダーを読み取り、残りの行を反復処理します。各行について、クラス cls
の from_row
メソッドを呼び出して、行のデータを使用してクラスのインスタンスを作成します。インスタンスは records
リストに追加され、関数はインスタンスのリストを返します。
この実験では、これらの関数をリファクタリングして、より柔軟で堅牢なものにします。また、Python の型ヒントシステムを探索します。これにより、関数のパラメータと戻り値の期待される型を指定することができます。これにより、特にコードを共同で作業する他の開発者にとって、コードの可読性が向上し、理解しやすくなります。
まず、reader.py
ファイルを作成し、これらの初期関数を追加しましょう。次の手順に進む前に、これらの関数が適切に動作することをテストするようにしてください。
まず、CSV データを読み取るための 2 つの基本的な関数を持つ reader.py
ファイルを作成しましょう。これらの関数は、データを辞書やクラスインスタンスに変換するなど、さまざまな方法で CSV ファイルを扱うのに役立ちます。
まず、CSV ファイルとは何かを理解する必要があります。CSV は Comma-Separated Values(カンマ区切り値)の略です。これは、表形式のデータを格納するために使用される単純なファイル形式で、各行が 1 つの行を表し、各行の値はカンマで区切られています。
では、reader.py
ファイルを作成しましょう。以下の手順に従ってください。
コードエディタを開き、/home/labex/project
ディレクトリに reader.py
という名前の新しいファイルを作成します。ここで、CSV データを読み取る関数を書きます。
reader.py
に以下のコードを追加します。
## reader.py
import csv
def read_csv_as_dicts(filename, types):
'''
CSV データを、オプションで型変換を行った辞書のリストに読み込む
引数:
filename: CSV ファイルへのパス
types: 各列の型変換関数のリスト
戻り値:
CSV ファイルのデータを含む辞書のリスト
'''
records = []
with open(filename) as file:
rows = csv.reader(file)
headers = next(rows)
for row in rows:
record = { name: func(val)
for name, func, val in zip(headers, types, row) }
records.append(record)
return records
def read_csv_as_instances(filename, cls):
'''
CSV データをクラスインスタンスのリストに読み込む
引数:
filename: CSV ファイルへのパス
cls: インスタンスを作成するクラス
戻り値:
CSV ファイルのデータを含むクラスインスタンスのリスト
'''
records = []
with open(filename) as file:
rows = csv.reader(file)
headers = next(rows)
for row in rows:
record = cls.from_row(row)
records.append(record)
return records
read_csv_as_dicts
関数では、まず open
関数を使用して CSV ファイルを開きます。次に、csv.reader
を使用してファイルを 1 行ずつ読み取ります。next(rows)
文は、通常ヘッダーが含まれるファイルの最初の行を読み取ります。その後、残りの行を反復処理します。各行について、キーがヘッダーで、値が行内の対応する値である辞書を作成し、types
リストを使用してオプションの型変換を行います。
read_csv_as_instances
関数も似ていますが、辞書を作成する代わりに、指定されたクラスのインスタンスを作成します。この関数は、クラスにデータの行からインスタンスを作成できる from_row
という静的メソッドがあることを前提としています。
test_reader.py
という名前の新しいファイルを作成します。## test_reader.py
import reader
import stock
## CSV を辞書として読み取るテスト
portfolio_dicts = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("First portfolio item as dictionary:", portfolio_dicts[0])
print("Total items:", len(portfolio_dicts))
## CSV をクラスインスタンスとして読み取るテスト
portfolio_instances = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
print("\nFirst portfolio item as Stock instance:", portfolio_instances[0])
print("Total items:", len(portfolio_instances))
test_reader.py
ファイルでは、先ほど作成した reader
モジュールと stock
モジュールをインポートします。次に、portfolio.csv
という名前のサンプル CSV ファイルを使用して 2 つの関数を呼び出してテストします。ポートフォリオの最初のアイテムとアイテムの総数を出力し、関数が期待どおりに動作していることを確認します。
python test_reader.py
出力は次のようになるはずです。
First portfolio item as dictionary: {'name': 'AA', 'shares': 100, 'price': 32.2}
Total items: 7
First portfolio item as Stock instance: Stock('AA', 100, 32.2)
Total items: 7
これにより、2 つの関数が正しく動作していることが確認されます。最初の関数は、CSV データを適切な型変換を行った辞書のリストに変換し、2 番目の関数は、指定されたクラスの静的メソッドを使用してクラスインスタンスを作成します。
次のステップでは、これらの関数をリファクタリングして、ファイル名だけでなく、任意の反復可能なデータソースで動作するようにし、より柔軟にします。
現在、私たちの関数はファイル名で指定されたファイルからの読み取りに限定されています。これは関数の使い勝手を制限しています。プログラミングでは、関数をより柔軟にして、さまざまなタイプの入力を扱えるようにすることが多くの場合有益です。私たちの場合、関数がファイルオブジェクトや他のソースなど、行を生成する任意の反復可能オブジェクトで動作するようになれば素晴らしいです。これにより、圧縮ファイルや他のデータストリームからの読み取りなど、より多くのシナリオでこれらの関数を使用できるようになります。
この柔軟性を実現するためにコードをリファクタリングしましょう。
reader.py
ファイルを開きます。新しい関数を追加するためにこのファイルを修正します。これらの新しい関数により、コードがさまざまなタイプの反復可能オブジェクトで動作するようになります。追加する必要があるコードは次のとおりです。## reader.py
import csv
def csv_as_dicts(lines, types):
'''
反復可能オブジェクトからの CSV データを辞書のリストに解析する
引数:
lines: CSV 行を生成する反復可能オブジェクト
types: 各列の型変換関数のリスト
戻り値:
CSV 行のデータを含む辞書のリスト
'''
records = []
rows = csv.reader(lines)
headers = next(rows)
for row in rows:
record = { name: func(val)
for name, func, val in zip(headers, types, row) }
records.append(record)
return records
def csv_as_instances(lines, cls):
'''
反復可能オブジェクトからの CSV データをクラスインスタンスのリストに解析する
引数:
lines: CSV 行を生成する反復可能オブジェクト
cls: インスタンスを作成するクラス
戻り値:
CSV 行のデータを含むクラスインスタンスのリスト
'''
records = []
rows = csv.reader(lines)
headers = next(rows)
for row in rows:
record = cls.from_row(row)
records.append(record)
return records
def read_csv_as_dicts(filename, types):
'''
CSV データを、オプションで型変換を行った辞書のリストに読み込む
引数:
filename: CSV ファイルへのパス
types: 各列の型変換関数のリスト
戻り値:
CSV ファイルのデータを含む辞書のリスト
'''
with open(filename) as file:
return csv_as_dicts(file, types)
def read_csv_as_instances(filename, cls):
'''
CSV データをクラスインスタンスのリストに読み込む
引数:
filename: CSV ファイルへのパス
cls: インスタンスを作成するクラス
戻り値:
CSV ファイルのデータを含むクラスインスタンスのリスト
'''
with open(filename) as file:
return csv_as_instances(file, cls)
コードをどのようにリファクタリングしたかを詳しく見てみましょう。
csv_as_dicts()
と csv_as_instances()
を作成しました。これらの関数は、CSV 行を生成する任意の反復可能オブジェクトで動作するように設計されています。これは、ファイル名で指定されたファイルだけでなく、さまざまなタイプの入力ソースを扱えることを意味します。read_csv_as_dicts()
と read_csv_as_instances()
を再実装して、これらの新しい関数を使用するようにしました。これにより、ファイル名でファイルから読み取る元の機能は引き続き利用可能ですが、より柔軟な関数をベースに構築されるようになりました。test_reader_flexibility.py
という名前のファイルを作成し、以下のコードを追加します。このコードは、さまざまなタイプの入力ソースで新しい関数をテストします。## test_reader_flexibility.py
import reader
import stock
import gzip
## 通常のファイルを開くテスト
with open('portfolio.csv') as file:
portfolio = reader.csv_as_dicts(file, [str, int, float])
print("First item from open file:", portfolio[0])
## 圧縮されたファイルを開くテスト
with gzip.open('portfolio.csv.gz', 'rt') as file: ## 'rt' はテキスト読み取りを意味する
portfolio = reader.csv_as_instances(file, stock.Stock)
print("\nFirst item from gzipped file:", portfolio[0])
## 後方互換性のテスト
portfolio = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("\nFirst item using backward compatible function:", portfolio[0])
test_reader_flexibility.py
ファイルがあるディレクトリに移動します。次に、以下のコマンドを実行します。python test_reader_flexibility.py
出力は次のようになるはずです。
First item from open file: {'name': 'AA', 'shares': 100, 'price': 32.2}
First item from gzipped file: Stock('AA', 100, 32.2)
First item using backward compatible function: {'name': 'AA', 'shares': 100, 'price': 32.2}
この出力は、関数が後方互換性を維持しながら、さまざまなタイプの入力ソースで動作することを確認しています。リファクタリングされた関数は、以下のソースからのデータを処理できます。
open()
で開かれた通常のファイルgzip.open()
で開かれた圧縮ファイルこれにより、コードははるかに柔軟になり、さまざまなシナリオで使用しやすくなります。
データ処理の世界では、すべての CSV ファイルが最初の行にヘッダーを持っているわけではありません。ヘッダーは、CSV ファイルの各列に付けられた名前で、各列がどのようなデータを保持しているかを理解するのに役立ちます。CSV ファイルにヘッダーがない場合、適切に処理する方法が必要です。このセクションでは、呼び出し元が手動でヘッダーを提供できるように関数を修正し、ヘッダーのある CSV ファイルとない CSV ファイルの両方を扱えるようにします。
reader.py
ファイルを開き、ヘッダー処理を含むように更新します。## reader.py
import csv
def csv_as_dicts(lines, types, headers=None):
'''
反復可能オブジェクトからの CSV データを辞書のリストに解析する
引数:
lines: CSV 行を生成する反復可能オブジェクト
types: 各列の型変換関数のリスト
headers: 列名のオプションリスト。None の場合、最初の行がヘッダーとして使用される
戻り値:
CSV 行のデータを含む辞書のリスト
'''
records = []
rows = csv.reader(lines)
if headers is None:
## ヘッダーが提供されない場合は、最初の行をヘッダーとして使用する
headers = next(rows)
for row in rows:
record = { name: func(val)
for name, func, val in zip(headers, types, row) }
records.append(record)
return records
def csv_as_instances(lines, cls, headers=None):
'''
反復可能オブジェクトからの CSV データをクラスインスタンスのリストに解析する
引数:
lines: CSV 行を生成する反復可能オブジェクト
cls: インスタンスを作成するクラス
headers: 列名のオプションリスト。None の場合、最初の行がヘッダーとして使用される
戻り値:
CSV 行のデータを含むクラスインスタンスのリスト
'''
records = []
rows = csv.reader(lines)
if headers is None:
## ヘッダーが提供されない場合は、最初の行をスキップする
next(rows)
for row in rows:
record = cls.from_row(row)
records.append(record)
return records
def read_csv_as_dicts(filename, types, headers=None):
'''
CSV データを、オプションで型変換を行った辞書のリストに読み込む
引数:
filename: CSV ファイルへのパス
types: 各列の型変換関数のリスト
headers: 列名のオプションリスト。None の場合、最初の行がヘッダーとして使用される
戻り値:
CSV ファイルのデータを含む辞書のリスト
'''
with open(filename) as file:
return csv_as_dicts(file, types, headers)
def read_csv_as_instances(filename, cls, headers=None):
'''
CSV データをクラスインスタンスのリストに読み込む
引数:
filename: CSV ファイルへのパス
cls: インスタンスを作成するクラス
headers: 列名のオプションリスト。None の場合、最初の行がヘッダーとして使用される
戻り値:
CSV ファイルのデータを含むクラスインスタンスのリスト
'''
with open(filename) as file:
return csv_as_instances(file, cls, headers)
これらの関数に加えた主要な変更点を理解しましょう。
すべての関数に headers
パラメータを追加し、そのデフォルト値を None
に設定しました。これは、呼び出し元がヘッダーを提供しない場合、関数がデフォルトの動作を使用することを意味します。
csv_as_dicts
関数では、headers
パラメータが None
の場合のみ、最初の行をヘッダーとして使用します。これにより、ヘッダーのあるファイルを自動的に処理できます。
csv_as_instances
関数では、headers
パラメータが None
の場合のみ、最初の行をスキップします。これは、独自のヘッダーを提供している場合、ファイルの最初の行は実際のデータであり、ヘッダーではないためです。
ヘッダーのないファイルでこれらの変更をテストしましょう。test_headers.py
という名前のファイルを作成します。
## test_headers.py
import reader
import stock
## ヘッダーのないファイルの列名を定義する
column_names = ['name', 'shares', 'price']
## ヘッダーのないファイルを読み取るテスト
portfolio = reader.read_csv_as_dicts('portfolio_noheader.csv',
[str, int, float],
headers=column_names)
print("First item from file without headers:", portfolio[0])
print("Total items:", len(portfolio))
## 同じファイルをインスタンスとして読み取るテスト
portfolio = reader.read_csv_as_instances('portfolio_noheader.csv',
stock.Stock,
headers=column_names)
print("\nFirst item as Stock instance:", portfolio[0])
print("Total items:", len(portfolio))
## 元の機能がまだ動作することを確認する
portfolio = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("\nFirst item from file with headers:", portfolio[0])
このテストスクリプトでは、まずヘッダーのないファイルの列名を定義します。次に、ヘッダーのないファイルを辞書のリストとして、およびクラスインスタンスのリストとして読み取るテストを行います。最後に、ヘッダーのあるファイルを読み取ることで、元の機能がまだ動作することを確認します。
python test_headers.py
出力は次のようになるはずです。
First item from file without headers: {'name': 'AA', 'shares': 100, 'price': 32.2}
Total items: 7
First item as Stock instance: Stock('AA', 100, 32.2)
Total items: 7
First item from file with headers: {'name': 'AA', 'shares': 100, 'price': 32.2}
この出力は、関数がヘッダーのある CSV ファイルとない CSV ファイルの両方を処理できることを確認しています。ユーザーは必要に応じて列名を提供することができ、または最初の行からヘッダーを読み取るデフォルトの動作に依存することもできます。
この変更により、CSV リーダー関数はより汎用的になり、より広範なファイル形式を処理できるようになりました。これは、コードをより堅牢でさまざまなシナリオで役立つものにするための重要なステップです。
Python 3.5 以降のバージョンでは、型ヒントがサポートされています。型ヒントは、コード内の変数、関数のパラメータ、および戻り値の期待されるデータ型を示す方法です。型ヒントはコードの実行方法を変更することはなく、コードをより読みやすくし、コードを実際に実行する前に特定の種類のエラーを検出するのに役立ちます。では、CSV リーダー関数に型ヒントを追加しましょう。
reader.py
ファイルを開き、型ヒントを含むように更新します。## reader.py
import csv
from typing import List, Callable, Dict, Any, Type, Optional, TextIO, Iterator, TypeVar
## クラスパラメータのジェネリック型を定義する
T = TypeVar('T')
def csv_as_dicts(lines: Iterator[str],
types: List[Callable[[str], Any]],
headers: Optional[List[str]] = None) -> List[Dict[str, Any]]:
'''
反復可能オブジェクトからの CSV データを辞書のリストに解析する
引数:
lines: CSV 行を生成する反復可能オブジェクト
types: 各列の型変換関数のリスト
headers: 列名のオプションリスト。None の場合、最初の行がヘッダーとして使用される
戻り値:
CSV 行のデータを含む辞書のリスト
'''
records: List[Dict[str, Any]] = []
rows = csv.reader(lines)
if headers is None:
## ヘッダーが提供されない場合は、最初の行をヘッダーとして使用する
headers = next(rows)
for row in rows:
record = { name: func(val)
for name, func, val in zip(headers, types, row) }
records.append(record)
return records
def csv_as_instances(lines: Iterator[str],
cls: Type[T],
headers: Optional[List[str]] = None) -> List[T]:
'''
反復可能オブジェクトからの CSV データをクラスインスタンスのリストに解析する
引数:
lines: CSV 行を生成する反復可能オブジェクト
cls: インスタンスを作成するクラス
headers: 列名のオプションリスト。None の場合、最初の行がヘッダーとして使用される
戻り値:
CSV 行のデータを含むクラスインスタンスのリスト
'''
records: List[T] = []
rows = csv.reader(lines)
if headers is None:
## ヘッダーが提供されない場合は、最初の行をスキップする
next(rows)
for row in rows:
record = cls.from_row(row)
records.append(record)
return records
def read_csv_as_dicts(filename: str,
types: List[Callable[[str], Any]],
headers: Optional[List[str]] = None) -> List[Dict[str, Any]]:
'''
CSV データを、オプションで型変換を行った辞書のリストに読み込む
引数:
filename: CSV ファイルへのパス
types: 各列の型変換関数のリスト
headers: 列名のオプションリスト。None の場合、最初の行がヘッダーとして使用される
戻り値:
CSV ファイルのデータを含む辞書のリスト
'''
with open(filename) as file:
return csv_as_dicts(file, types, headers)
def read_csv_as_instances(filename: str,
cls: Type[T],
headers: Optional[List[str]] = None) -> List[T]:
'''
CSV データをクラスインスタンスのリストに読み込む
引数:
filename: CSV ファイルへのパス
cls: インスタンスを作成するクラス
headers: 列名のオプションリスト。None の場合、最初の行がヘッダーとして使用される
戻り値:
CSV ファイルのデータを含むクラスインスタンスのリスト
'''
with open(filename) as file:
return csv_as_instances(file, cls, headers)
コードに加えた主要な変更点を理解しましょう。
typing
モジュールから型をインポートしました。このモジュールは、型ヒントを定義するために使用できる型のセットを提供します。たとえば、List
、Dict
、および Optional
はこのモジュールの型です。
クラス型を表すためにジェネリック型変数 T
を追加しました。ジェネリック型変数を使用すると、型安全な方法でさまざまな型で動作できる関数を記述できます。
すべての関数のパラメータと戻り値に型ヒントを追加しました。これにより、関数が期待する引数の型と返す値の型が明確になります。
List
、Dict
、および Optional
などの適切なコンテナ型を使用しました。List
はリストを表し、Dict
は辞書を表し、Optional
はパラメータが特定の型を持つか、または None
であることを示します。
型変換関数に Callable
を使用しました。Callable
は、パラメータが呼び出し可能な関数であることを示すために使用されます。
ジェネリック型 T
を使用して、csv_as_instances
が渡されたクラスのインスタンスのリストを返すことを表現しました。これにより、IDE や他のツールが返されるオブジェクトの型を理解するのに役立ちます。
すべてが正常に動作することを確認するために、簡単なテストファイルを作成しましょう。
## test_types.py
import reader
import stock
## 関数は以前とまったく同じように動作するはず
portfolio = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("First item:", portfolio[0])
## しかし、今ではより良い型チェックと IDE サポートがある
stock_portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
print("\nFirst stock:", stock_portfolio[0])
## stock_portfolio が Stock オブジェクトのリストであることがわかる
## これにより、IDE がより良いコード補完を提供できる
first_stock = stock_portfolio[0]
print(f"\nName: {first_stock.name}")
print(f"Shares: {first_stock.shares}")
print(f"Price: {first_stock.price}")
print(f"Value: {first_stock.shares * first_stock.price}")
python test_types.py
出力は次のようになるはずです。
First item: {'name': 'AA', 'shares': 100, 'price': 32.2}
First stock: Stock('AA', 100, 32.2)
Name: AA
Shares: 100
Price: 32.2
Value: 3220.0
型ヒントはコードの実行方法を変更しませんが、いくつかの利点があります。
大規模なコードベースでは、これらの利点によりバグを大幅に減らし、コードを理解しやすく保守しやすくすることができます。
注意: 型ヒントは Python ではオプションですが、プロのコードではますます使用されるようになっています。Python 標準ライブラリや多くの人気のあるサードパーティパッケージなどのライブラリには、広範な型ヒントが含まれるようになっています。
この実験では、Python の関数設計に関するいくつかの重要な側面を学びました。まず、基本的な関数設計、具体的には CSV データをさまざまなデータ構造に処理する関数の書き方を学びました。また、関数をリファクタリングして任意の反復可能なソースで動作するようにすることで、関数の柔軟性を探り、コードの汎用性と再利用性を高めました。
さらに、ヘッダーの有無などの異なるユースケースを処理するためのオプションパラメータの追加や、Python の型ヒントシステムを使用したコードの読みやすさと保守性の向上を習得しました。これらのスキルは、堅牢な Python コードを書くために不可欠であり、プログラムが複雑になるにつれて、これらの設計原則がコードを整理し、理解しやすく保つでしょう。これらの技術は CSV 処理以外にも適用できるため、Python プログラミングのツールキットにとって非常に価値があります。