名前付きでタプルにアクセスできるcollectionsモジュールのnamedtupleについて解説します。
namedtuple(collectionsモジュール)
Pythonの代表的な型であるタプル(tuple)では、アクセスする場合に[]で位置を指定してアクセスしたり、アンパック代入で変数に代入して使ったりすることができます。
しかし要素数が多くなってくると、どの位置に何の情報が入っているのかが分かりにくくなってしまうため、各値に名前付きでアクセスできると便利な場合があります。
このような場合には、名前付きタプルであるnamedtupleが使用できます。namedtupleは、collectionsモジュールに用意されています。
本記事では、collectionsモジュールのnamedtupleについて定義方法や使い方、namedtupleの使いどころや特徴といった点について説明します。
namedtupleの定義と使い方
namedtupleの定義と使い方を、以下の簡単な例で定義や使い方について見ていきましょう。
import collections # named tupleの定義 Point = collections.namedtuple("Point", ["x", "y"]) # インスタンス化 p1 = Point(10, 20) p2 = Point(x=30, y=40) print(f"p1 = {p1}") print(f"p2 = {p2}") # フィールド名で値へアクセス print(f"p1.x = {p1.x}, p1.y = {p1.y}") print(f"p2.x = {p2.x}, p1.y = {p2.y}") # アンパック代入 x1, y1 = p1 x2, y2 = p2 print(f"x1 = {x1}, y1 = {y1}") print(f"x2 = {x2}, y2 = {y2}")
【実行結果】 p1 = Point(x=10, y=20) p2 = Point(x=30, y=40) p1.x = 10, p1.y = 20 p2.x = 30, p1.y = 40 x1 = 10, y1 = 20 x2 = 30, y2 = 40
上記のサンプルプログラムについてポイントを説明します。
import collections
まず、namedtupleは以下のようにcollectionsモジュールのインポートが必要です。
# named tupleの定義 Point = collections.namedtuple("Point", ["x", "y"])
namedtupleの定義では、タイプの名前とフィールドの名前といったものを定義します。上記の例では、”Point”という名前のタイプを定義し、フィールドとして”x”, “y”という名前のフィールドをリストで指定して定義しています。
# インスタンス化 p1 = Point(10, 20) p2 = Point(x=30, y=40) print(f"p1 = {p1}") print(f"p2 = {p2}")
実際に定義したnamedtupleをインスタンス化するためには上記のように記載します。”x”, “y”に該当する値を順に引数に指定してもよいですし、x=, y=のように設定したフィールド名のキーワード引数として指定することも可能です。
# フィールド名で値へアクセス print(f"p1.x = {p1.x}, p1.y = {p1.y}") print(f"p2.x = {p2.x}, p1.y = {p2.y}")
値にアクセスする場合には、インスタンス化した変数に対して.(ドット)でアクセスすることができます。
# アンパック代入 x1, y1 = p1 x2, y2 = p2 print(f"x1 = {x1}, y1 = {y1}") print(f"x2 = {x2}, y2 = {y2}")
また、namedtupleは名前付きのタプルですので通常のタプルと同様にアンパック代入によって値を取り出してデータを扱うことも可能です。
以上が、namedtupleの基本的な使い方になります。以降では、namedtupleで使用できる便利な各種メソッドについて紹介します。
namedtupleの各種メソッド
namedtupleでは便利な各種メソッドが用意されています。namedtupleのメソッド名は、公式ドキュメントに記載がありますが、フィールド名との衝突を避けるために「_(アンダースコア)」で始まる名前となっています。
一般的には_(アンダースコア)がついた変数やメソッドは、慣例としてクラス内のみで参照・使用される変数やメソッドというプライベートの意味があります(ただし、外部からのアクセスは可能です)が、namedtupleは少し異なっていますね。
_makeで新しいnamedtupleを作成する
既存リストからnamedtupleを作成する場合には、以下のように_makeメソッドを使用します。
import collections Point = collections.namedtuple("Point", ["x", "y"]) data = [10, 20] p_data = Point._make(data) print(f"p_data = {p_data}") print(p_data.x, p_data.y)
【実行結果】 p_data = Point(x=10, y=20) 10 20
Pointという定義を作る点は基本的な使い方で紹介した通りですが、上記例ではdataというリストで定義されている値をもとにして、namedtupleのインスタンスp_dataを作成しています。
_asdictでnamedtupleを辞書に変換する
作成したnamedtupleを辞書に変換して使用したい場合は、以下のように_asdictメソッドを使用します。
import collections Point = collections.namedtuple("Point", ["x", "y"]) p1 = Point(10, 20) p_dict = p1._asdict() print(type(p_dict)) print(p_dict) print(p_dict["x"]) print(p_dict["y"])
【実行結果】 <class 'dict'> {'x': 10, 'y': 20} 10 20
_asdict()の返却値をtypeで確認していますが、dictとなっていることが分かるかと思います。このようにdictへ変換して扱うことが可能です。
_asdict()の返却値は、バージョン3.1でdictの代わりにOrderedDictを返すようになりましたが、その後バージョン3.8からdictに戻っています。
これはバージョン3.7で、通常のdictが順序付けられることが保証されたためです。OrderedDictの追加機能が必要な場合は、OrderDictにOrderedDict(x._asdict())といった形でキャストするように勧められています。
この内容は公式ドキュメントのこちらに記載があります。
_replaceで一部の値を変更した新しいnamedtupleを作成する
作成したnamedtupleをもとに一部の値を変更した新しいnamedtupleのインスタンスを作成する場合には、以下のように_replaceメソッドを使用します。
import collections Point = collections.namedtuple("Point", ["x", "y"]) p1 = Point(10, 20) # p1のxの値を置き換える print(p1._replace(x=50)) # p1の値が書き換わるわけではないので注意 print(p1, "\n") # 値を書き換えた場合は別のインスタンスに代入する p2 = p1._replace(x=50) print(f"p1 = {p1}") print(f"p2 = {p2}")
【実行結果】 Point(x=50, y=20) Point(x=10, y=20) p1 = Point(x=10, y=20) p2 = Point(x=50, y=20)
タプルはイミュータブルで変更不可なものなので、_replaceメソッドは既存のnamedtupleそのものの値を置き換えるわけではなく、値を置き換えたnamedtupleのインスタンスの変数を返却することに注意してください。
そのため、使用する場合には別の変数に代入して使用する必要があります。
_fieldsによりフィールド名を取得する
namedtupleのフィールド名を取得するには、以下のように_fieldsメソッドを使用します。
import collections Point = collections.namedtuple("Point", ["x", "y"]) p1 = Point(10, 20) print(p1._fields)
【実行結果】 ('x', 'y')
上記のように返却値は、フィールド名が列挙されたタプルで返却されます。
namedtupleの使いどころ
CSVファイルの読み込みに使用する
namedtupleの使いどころとしてよく紹介されるCSVファイル値取得での利用について例を使って紹介します。
import collections import csv # CSVファイルを作成して保存する with open("temp.csv", "w", newline="", encoding="UTF-8") as csv_write: fields = ["index", "value1", "value2", "value3"] writer = csv.DictWriter(csv_write, fieldnames=fields) writer.writeheader() for i in range(1, 6): writer.writerow( { "index": i, "value1": f"val1-{i}", "value2": f"val2-{i}", "value3": f"val3-{i}", } ) # CSVファイルをnamedtupleに読み込む with open("temp.csv", "r", encoding="UTF-8") as csv_read: csv_reader = csv.reader(csv_read) # namedtupleを定義する(CSVの1行目を列名として定義) record = collections.namedtuple("record", next(csv_reader)) # CSVのレコードを順次読み込む (namedtupleで1行を読み込む) data = [record._make(row) for row in csv_reader] print(data) # 1行目のデータに列名でアクセスする print(data[0].index) print(data[0].value1) print(data[0].value2) print(data[0].value3)
【実行結果】 [record(index='1', value1='val1-1', value2='val2-1', value3='val3-1'), record(index='2', value1='val1-2', value2='val2-2', value3='val3-2'), record(index='3', value1='val1-3', value2='val2-3', value3='val3-3'), record(index='4', value1='val1-4', value2='val2-4', value3='val3-4'), record(index='5', value1='val1-5', value2='val2-5', value3='val3-5')] 1 val1-1 val2-1 val3-1
CSVファイルを読み込む際には、CSVファイルの列名をnamedtupleで定義しておき、データを読み込む際にnamedtupleに設定することで、その後データに列名で参照することができます。
この例では、まずサンプルのCSVを作成してtemp.csvで書き込み、作ったデータをnamedtupleに読み込んでいます。
next(csv_reader)の部分で、CSVのヘッダーの列名情報を取得できるため、その値を使って”record”というnamedtupleを定義しています。
その後データを読み込む際には、リスト内包表記を使って_makeメソッドでnamedtupleにしつつdataというリストを作成しています。
データへのアクセスは、列名である「index」「value1」「value2」「value3」を用いて直接それぞれにアクセスできていることが分かると思います。このようにしておくことで読み込んだデータ列を指定してデータを扱うことが容易になります。
上記で出てきたCSVファイル入出力やリスト内包表記については以下にまとめていますので分からない方などは参考にしていただければと思います。
関数の戻り値で使用する
関数の戻り値として複数の戻り値を返却する場合は、返却値が編集されてしまわないようにイミュータブルなタプルに返却値に設定するのが基本です。その際に、どれが何の値なのかを明示するためにnamedtupleを使うと便利です。
以下の例のcalculate_stat関数は、入力された数値リストの最小値、最大値、平均、分散、標準偏差を計算して返却する関数です。
※本来このままだどdataに数値以外が入ってくるとエラーになるのでよいプログラムではありませんが、namedtupleを返却する例ということで、細かなチェック等は割愛しています。
import collections import statistics def calculate_stat(data): min_v = min(data) max_v = max(data) mean_v = statistics.mean(data) var_v = statistics.variance(data) std_v = statistics.stdev(data) # 返却値をnamedtupleで定義 Stat = collections.namedtuple("Stat", ["min", "max", "mean", "var", "std"]) stats = Stat(min_v, max_v, mean_v, var_v, std_v) return stats def main(): values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] result = calculate_stat(values) print( f"min:{result.min}, " f"max:{result.max}, " f"mean:{result.mean}, " f"var:{result.var}, " f"std:{result.std}" ) if __name__ == "__main__": main()
【実行結果】 min:1, max:15, mean:8, var:20, std:4.47213595499958
上記例では返却値を”Stat”というタイプのnamedtupleにまとめて設定して返却しています。
通常のタプルで返却すると、何番目に最小値が入ってくるかといったことを覚えていないといけませんが、namedtupleを使用することで、呼び出した側は最小値ならmin、最大値ならmaxといったように直感的に返却値を扱うことができます。
このように関数の戻り値としてnamedtupleを使用することがあります。
namedtupleの特徴
上記ではnamedtupleの定義や使い方について見てきました。ここではnamedtupleの特徴について紹介します。
namedtupleと辞書(dict)の違い
これまでの使用例を見て名前を用いて要素にアクセスできることから、namedtupleがPython標準のタプル(tuple)よりも理解しやすく、扱いやすいということはご理解いただけたかと思います。しかし、同じことはPython標準の辞書(dict)でも同じようにキーでアクセスできるように思います。
namedtupleと辞書(dict)で異なる点は以下のような点です。
【namedtupleと辞書(dict)の違い】
- namedtupleはタプルの一種なのでイミュータブルであり内部の長さは必要サイズに固定されている。一方で辞書(dict)は必要な領域を余分に確保(オーバーアロケート)しているためメモリの使用効率ではnamedtupleの方がよい。
- namedtupleは添え字でもアクセス可能で、この操作は高速にできる。辞書(dict)は内部構造としてハッシュテーブルの探索が必要となり最悪のケースだと遅くなる可能性がある。
パフォーマンスが要求されるようなコードの部分では添え字のインデックスを使ってアクセスすることで応答性を確保し、その他の場面では属性名でアクセスできる利便性を享受するといったことができる点がnamedtupleの特徴です。
まとめ
名前付きでタプルにアクセスできるcollectionsモジュールのnamedtupleについて解説しました。
名前付きタプルは、タプルの各要素に名前でアクセスできるため、関数の戻り値で使用したり、CSVの読み込みに使用すると便利です。各種メソッドも用意されており、本記事で代表的なメソッドを紹介しています。
namedtupleは、辞書でキーにアクセスすることと似ていますが、namedtupleはタプルの一種のためイミュータブルで内部のメモリ使用効率が良くなります。また、namedtupleは添え字によるアクセスも可能で、この操作はアクセスが高速なため状況によって使い分けるといったこともできます。
名前付きでアクセスできるnamedtupleは便利な面が多いため、是非状況によって使用を検討してもらえればと思います。
namedtupleの公式ドキュメントはこちらを参照してください。
上記で紹介しているソースコードについてはgithubにて公開しています。参考にしていただければと思います。