namedtupleの解説記事です。
この記事の想定読者
- namedtupleの基本を知りたい人
- namedtupleの存在意義がよく分からない人
- namedtupleの活用場面を知りたい人
- そこそこ多量のデータを上手に管理する方法を探している人
【2019年10月4日追記】
結構な数のいいねをいただいたため、きちんとした記事にしなければと改めて思っております。
見直しますと、typing.NamedTuple
の利用方法など、重要な情報が不足していると思いましたので(執筆当時の知識不足によります。申し訳ありません)、本日追記して記述を補いました。
はじめに
あまりメジャーではないかもしれませんが、python
にはnamedtuple
というデータ型があります。標準ライブラリのcollections
モジュールからインポートできます。
私自身はnamedtuple
に関しては、
- dictionaryのようにキーバリュー的に値を格納できる
- ドットアクセスできる。あの面倒な
dic["key"]
を書かないでいい!! - 基本的にtupleである。なのでイミュータブルである
- tupleなので軽量だったり早かったりするのではないか?
というくらいの認識でした。
私はついついパフォーマンスが気になるし、イミュータブルと言われると安心を感じるほうなので、namedtuple
はなんだか気になる存在でした。ただ、ネットで検索してみてもあまりすっきりした解説に出会うことはなく、自己流でもやもやしていました。
そんなある日、namedtuple
はとても「安全」なんじゃないかと思い、**"namedtuple safe"**というキーワードでググってみました。そこで以下の記事を発見しました。
Namedtupleでクリーンなpythonを書く
Writing Clean Python With Namedtuples
読んでみたところ、namedtuple
の入門に必要な内容がきちんと書かれており、更に長所や使いどころも整理されており、私的にはスッキリな記事でした。
namedtuple
に関しては、日本語の解説記事でほどよいものを見た記憶がなかったので、この際、記事を和訳して投稿してみようと思い立ちました。
なお、翻訳については著者の了解を得ています。
それではお楽しみください。
namedtupleでクリーンなpython
を書く(翻訳)
python
には namedtuple
という特別なコンテナがありますが、その力が十分に評価されていないようです。python
が持つ素晴らしい機能なのに、みんなに見過ごされているものの1つです。
クラスを自分で定義するかわりにnamedtuple
を使うことが非常に有効です。そしてnamedtuple
には他にも面白い特徴があります。この記事でそれを紹介したいと思います。
さて、namedtuple
とはなんでしょうか。また、何が特別なのでしょうか。
よい方法があります。組み込みのデータ型であるtuple
の拡張だと考えてみるのです。
python
のtuple
はシンプルなデータ型で、任意のオブジェクトをグループにします。また、tuple
はイミュータブルです。一旦作成されると変更できません。
>>> tup = ('hello', object(), 42)
>>> tup
('hello', <object object at 0x105e76b70>, 42)
>>> tup[2]
42
>>> tup[2] = 23
TypeError: "'tuple' object does not support item assignment"
プレーンなtuple
には弱点があります。格納したデータに、整数のインデックスでしかアクセスできないことです。tuple
に格納された個々のプロパティに名前を付けることはできません。これはコードの可読性を損ないます。
また、tuple
の構造は毎回変わります。2つのtuple
が、同じ数のフィールドと、同じプロパティを持っている保証はありません。フィールドの順番をうっかり取り違えてバグを起こしがちです。
namedtupleが解決!
namedtuple
の狙いは、この2つの問題を解決することです。
何よりも、namedtuple
は通常のtupleと同じようにイミュータブルです。一旦何かを格納すると、もう変更することはできません。
それと同時にnamedtuple
は・・・、そう、namedなtuple
なのです。
namedtuple
に格納されたオブジェクトには、ユニークな(人間に読みやすい)名前でアクセスすることができます。数値のインデックスを覚える必要はありません。インデックスで使うために数値の入った定数を定義するような、その場しのぎも必要ありません。
namedtuple
はこんな風に使います。
>>> from collections import namedtuple
>>> Car = namedtuple('Car' , 'color mileage')
namedtuple
を使うためには、collections
モジュールをインポートします。Python2.6で標準ライブラリに追加されました。上の例では、Carというシンプルなデータ型を定義しました。Car
にはcolor
とmileage
の2つのフィールドがあります。
書き方が少し妙に見えるかもしれません。なぜフィールドを"color mileage"という文字列で渡すのでしょう。
それはnamedtuple
のファクトリ関数が、この文字列に対して、split()
を呼び出すからです。つまり、以下の書き方の略記になっています。
>>> 'color mileage'.split()
['color', 'mileage']
>>> Car = namedtuple('Car', ['color', 'mileage'])
もちろん、そのほうが好みなら、リストでそのまま渡すこともできます。このやり方のメリットは、コードを複数行に分けるときに簡単に書き直せることです。
>>> Car = namedtuple('Car', [
... 'color',
... 'mileage',
... ])
いずれにしても、これでCar
ファクトリ関数によって、新しいcar
オブジェクトを作ることができるようになりました。これは、Car
クラスを自分で定義し、color
とmileage
の値を受け付けるコンストラクタを作ったのと同じ振る舞いになります。
(【2019年10月4日追記:訳注】python3.6.1より、typing.NamedTuple
を使う読みやすい記法が実装されています(後述します)。)
>>> my_car = Car('red', 3812.4)
>>> my_car.color
'red'
>>> my_car.mileage
3812.4
アンパック代入や、可変長引数(*)も利用できます。
>>> color, mileage = my_car
>>> print(color, mileage)
red 3812.4
>>> print(*my_car)
red 3812.4
namedtuple
の値には名前でアクセスできる上に、インデックスでも変わらずアクセスできます。このため、namedtuple
はプレーンなtuple
をいつでも置き換えることができます(drop-in replacement)。
>>> my_car[0]
'red'
>>> tuple(my_car)
('red', 3812.4)
特別な設定なく、いい具合の文字表示まで使えるようになります。タイプ量も冗長さもおさえられます。
>>> my_car
Car(color='red' , mileage=3812.4)
プレーンなtuple
と同様に、namedtuple
はイミュータブルです。フィールドを上書きしようとすると、AttributeError
になります。
>>> my_car.color = 'blue'
AttributeError: "can't set attribute"
namedtuple
オブジェクトは、内部的に、pythonの通常のクラスとして実装されます。メモリ効率に関しては、通常のクラスよりも良好であり、プレーンなtuple
と同水準です。
こう考えるとよいでしょう。namedtuple
はメモリ効率がよいイミュータブルなクラスです。そして、クラス定義のショートカットになります。
namedtupleをサブクラス化する
namedtuple
は通常のクラスをベースにしているため、namedtuple
のクラスにメソッドを追加することもできます。たとえば、他のクラスと同様に継承することができますし、メソッドやプロパティも同様に追加できます。こんな具合です。
>>> Car = namedtuple('Car', 'color mileage')
>>> class MyCarWithMethods(Car):
... def hexcolor(self):
... if self.color == 'red':
... return '#ff0000'
... else:
... return '#000000'
MyCarWithMethods
クラスを作り、hexcolor()
メソッドを呼び出しました。期待通りに動作します。
>>> c = MyCarWithMethods('red', 1234)
>>> c.hexcolor()
'#ff0000'
少し簡単すぎたかもしれませんね。イミュータブルなプロパティのあるクラスが欲しいときに、こうするとよいかもしれません。でもずっこけたことをやってしまうものです。
たとえば、新たにイミュータブルなフィールドを追加するのは、namedtuple
の実装からすると少し難しいです。namedtuple
のクラス階層を作る一番簡単な方法は、基底となるtuple
の ._fields
プロパティを使うことです。
>>> Car = namedtuple('Car', 'color mileage')
>>> ElectricCar = namedtuple(
... 'ElectricCar', Car._fields + ('charge',))
これで期待通りになります。
>>> ElectricCar('red', 1234, 45.0)
ElectricCar(color='red', mileage=1234, charge=45.0)
組み込みのヘルパーメソッド
_fields
プロパティのほかにも、namedtuple
のインスタンスにはいくつかのヘルパーメソッドがあります。便利に使えるかも知れません。
ヘルパーメソッドの名前は全てアンダースコアから始まります。
アンダースコアは普通はそのメソッドやプロパティがプライベートであることを意味し、クラスやモジュールのパブリックインターフェースを意味しません。
namedtuple
でアンダースコアが使われるのは別の意味があります。これらのヘルパーメソッドやプロパティは、namedtuple
のパブリックインターフェースになっています。ユーザーが定義するフィールド名との衝突を避けるために、アンダースコアがついています。
namedtuple
のヘルパーメソッドが便利に活用できるシナリオをいくつか紹介します。最初は_asdict()
です。namedtuple
の中身をdict
で返します。
>>> my_car._asdict()
OrderedDict([('color', 'red'), ('mileage', 3812.4)])
json
を出力するときにタイポしないですみますね。たとえばこんな具合です。
>>> json.dumps(my_car._asdict())
'{"color": "red", "mileage": 3812.4}'
次の便利なヘルパーは_replace()
関数です。tuple
のシャローコピーを返し、選んだフィールドを置き換えます。
>>> my_car._replace(color='blue')
Car(color='blue', mileage=3812.4)
最後に、_make()
クラスメソッドはシーケンスやイテレーターから、新しいインスタンスを作るために使えます。
>>> Car._make(['red', 999])
Car(color='red', mileage=999)
どんなときにnamedtupleを使うか
namedtuple
は、コードをきれいに、読みやすくする簡単な方法になります。データにきちんとした型を与えられるからです。
たとえば、定型フォーマットにしたディクショナリ(そのたびに型が変わるものです)を、namedtuple
に変えることで、自分の意図がよりはっきり表現できます。このリファクタリングをやってみると、目下の問題に素晴らしい解決策をたちまち考えつくことがしばしばです。
構造の無いタプルやディクショナリでなくnamedtuple
を使うことで、同僚たちの仕事も楽になります。データがある程度セルフドキュメンティングな形で渡されるからです。
一方、よりクリーンに、読みやすく、メンテナンスしやすいコードにするために役立たないなら、namedtuple
は使わないようにしています。過ぎたるは及ばざるがごとしです。
しかし意識して使えば、namedtuple
はまちがなく、あなたのpythonコードをより良く、そして、表現力豊かなものにしてくれます。
覚えておこう!
-
collection.namedtuple
は自分でイミュータブルなクラスを定義するショートカットになり、しかも、メモリ効率がよい。 -
namedtuple
はデータに理解しやすい型を与えることで、コードをクリーンにしてくれる。 -
namedtuple
には便利なヘルパーメソッドがある。アンダースコアから始まるが、パブリックインターフェースだ。これを活用するのもよい。
投稿者より
私自身はnamedtuple
であれこれ試している段階です。
これまではちょっとしたデータの格納にはいつもdict
を使っていたのですが、それを積極的にnamedtuple
に置き換えるようになりました。
データコンテナとして
よく考えてみると、型(キー)が固定しているデータって結構よくあるのですね。固定していることが期待されるというべきでしょうか。
そういうデータは、キーが本質的に不定なdict
よりも、namedtuple
であるほうが自然だと思います。
それと、イミュータブルという点が魅力です。
ありませんか? 謎のバグが起きて、必死でトレースバックを追ってみると、「うわ、ここでdictの値が書き換わってた・・・」みたいなことが。
ごく短いスコープで使うならばまだしも、クラスや関数間で受け渡すような場合は、どうもミュータブルは不安になります。データ用のクラスもミュータブルだと不安です。
こういうのをnamedtuple
で置き換えてみると、すっきり安心になりました。
最近使った例では、
- ディレクトリのパスを格納するため
- 8個セットの引数を受け渡すため
- システム内で変更されることがない基本データの格納のため
といった感じです。
パスの格納はこんなイメージです。ファクトリとセットにしています。
import os
from collections import namedtuple
PathContainer = namedtuple("PathContainer","root document exe")
def generate_PathContainer(root):
document = os.path.join(root,"document")
exe = os.path.join(root,"hoge","fuga.exe")
return PathContainer(root,document,exe)
勝手に書き換わることが決してない!と思うと、すごく安心して寄りかかれます。
5個の関数の間でミュータブルな引数を次々と渡していくとかちょっと怖かったですが、namedtuple
にしてから大丈夫だと思えるようになりました。
【2019年10月4日追記】
この記事を書いた後も、筆者はご機嫌でnamedtuple
を活用しています。その後の知見として、関数の戻り値として使うというのは有力だと思います。
pythonはこんな風にして、関数の戻り値として、簡単にtuple
を返せます。
def analyze_text(text):
# some_process
name = "analyzed_name"
age = processed_number
return name,age
ただこれが意外と曲者で、考え無しに使っていると、似た関数でname
だけ返していたり、age,name
を返したり、name,address
を返したり、、といった具合に戻り値が混沌としていくことがあります。
こういう場合、戻り値の型をnamedtuple
で定義してしまえば、一気にすっきりします(この記法はすぐあとで説明します)。
from typing import NamedTuple
class ResultAnalyze(NamedTuple):
name: str
age: int
def analyze_text(text):
# some_process
name = "analyzed_name"
age = processed_number
return ResultAnalyze(name,age)
こうしておけば、戻り値がバラバラになるなんてことは起きなくなります。
本体記事にもあったように、namedtuple
は数値インデックスでのアクセスもできるなど、tuple
と同じインターフェースを持っています。ですので、既存の関数をこういう風にリファクタしても、既存のコードに影響を与えることなく動きます。
【追記終わり】
ドットアクセス
それと、ドットアクセスはやはり魅力です。見やすい、書きやすいですよね。
path_container["exe"]
VS
path_container.exe
更にドットアクセスならば、エディターによる入力補完がききやすいです。
ちなみに引数を動的にしたいときには、getattr
を使えばOKです。
path_container = generate_PathContainer("ROOTPATH")
key = "exe"
getattr(path_container, key)
# ROOTPATH\hoge\fuga.exe
デフォルト値を設定する
namedtuple
はデフォルト値を設定することもできます。
__new__.__defaults__
を使えばOKです。これで便利に活用できますね。
Point = namedtuple("Point","name x y")
Point.__new__.__defaults__ = ("name",0,0)
# tupleを作れば良いので、こうやってNoneで埋める手も
Point.__new__.__defaults__ = tuple([None]*3)
typing.NamedTupleによる記述法【2019年10月4日追記】
この記事を書いた当時は把握できていなかったのですが、python3.6.1より、typing.NamedTuple
を用いた記法が実装されています。以下のような書き方です。
from typing import NamedTuple
class PathContainer(NamedTuple):
root: str
document: str
exe: str
path_container = PathContainer("root_path","document_path","exe_path")
root: str
などと書いてあるのは、型ヒントです(pythonで型を指定するための仕様です)。この記法では、型ヒントが必須となっています。
行数は少し伸びますが、PathContainer = namedtuple("PathContainer","root document exe")
よりもわかりやすいと感じる方もおられるのではないでしょうか。
更に以下の方法で、簡単にデフォルト値が設定できます。
from typing import NamedTuple
class PathContainer(NamedTuple):
root: str = "root_path"
document: str = "document_path"
exe: str = "exe_path"
path_container = PathContainer()
print(path_container)
# output => PathContainer(root='root_path', document='document_path', exe='exe_path')
筆者自身は、この記法を把握してからは、namedtupleは全てこの書き方で書くようになりました。それと、筆者はPyCharmを使っているのですが、型方式で書くとPyCharmがnamedtupleをきちんとclassとして認識してくれるので、入力補完がばっちりききます。この点も大きな魅力です。
公式の説明はこちらです。
【追記終わり】
パフォーマンス(速度)
速度面のパフォーマンスですが、sqlite
と併用すると、初期化が早いというメリットがあります(後記)。
ただ、それ以外の場面でははっきりしたメリットは見つかりませんでした。
計測してみたところ、キーによるデータアクセスは、だいたいdict
の2分の1倍速(=2倍の時間がかかる)です。通常のクラスと比較しても低速でした。
インスタンス化の速度もdictより何割か低速です。
実用上問題のあるレベルではないですが、ヘビーなループがあるときなどは、dictを選ぶかもしれません。
sqliteのデータコンテナに活用
sqlite
からデータを読み出して、リストに格納することがありますよね。
ここでnamedtuple
を使うのはおすすめできそうです。
せっかくですので、少し丁寧に解説します。
書き方
まず結論です。こういう書き方になります。
import sqlite3
from contextlib import closing
from collections import namedtuple
DBPATH = r"C:\xxxxx\yyyyy\dbfile.sqlite"
with closing(sqlite3.connect(DBPATH)) as conn:
cursor = conn.cursor()
SQL = "select * from table1"
cursor.execute(SQL)
fieldname_list = [field[0] for field in cursor.description]
RowNamedtuple = namedtuple("RowNamedtuple", fieldname_list)
rows = [RowNamedtuple._make(row) for row in cursor] #Rownamedtuple(row)はダメ!
cursor.execute(SQL)
でデータを読み出します。その結果から、フィールド名を取得し、それをもとにnamedtuple
のファクトリを作成します。そして、ファクトリの._make
を使って、各レコードを初期化する、という流れです。
[field[0] for field in c.description]
がちょっと分かりにくいかもしれません。
この部分は、select
結果に含まれるフィールド名をリストにして取り出しています。そして、namedtuple
の第2引数にそれを渡して、テーブルと同じフィールドを持つnamedtuple
を作っています。
c.description
は、select
されたフィールド名を格納しています。
じゃあ、field[0]
の[0]
はなんだということになるのですが、実はc.description
の中身は
( ("filedname1",None,None,None,None,None,None), ("filedname2",None,None,None,None,None,None),... )
という形式のタプルになっているのです。これはPython DB APIとの互換性を保つためだそうです。field[0]
でこの0番だけを読み取っています。
仕様についてはドキュメントに解説がありますので参考にしてください。
sqlite3 cursor.description
最後はRowNamedtuple._make(row)
となることに注意してください。RowNamedtuple(row)
では初期化できません。私はここで30分くらいハマったことがあります。
(デフォルトでは、cursor
をイテレートすると、tuple
が返ってくるんですね。namedtuple
は、引数を1個ずつ渡す必要があるので(可変長引数スタイル)、tuple
をそのまま渡しても正しく初期化できないです。
こういう風に、「そのまま渡したい」ときが、まさに_make
の出番になります。)
なお、RowNamedtuple(*row)
と書くこともできますが、パフォーマンスは少し低下します。
メリット
同じ場面で、sqlite3.Row
のまま使ったり、更にdict
に変換することもできますが、namedtuple
には以下のような特徴があります。
- ドットアクセスができる
-
dict
よりも初期化が早い - イミュータブルである
1番目、2番目は、はっきりうれしい点です。3番目は場面によってありがたいです。
パフォーマンス
サンプルコードを兼ねて、初期化速度の計測結果を掲げておきます。
計測に用いたsqliteファイルは15000レコードのものです。
コードはこれです。
def read_by_plain():
with closing(sqlite3.connect(DB)) as conn:
cursor = conn.cursor()
cursor.execute(SQL)
return [row for row in cursor] #プレーンタプルが返る
def read_by_Row():
with closing(sqlite3.connect(DB)) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(SQL)
return [row for row in cursor] #sqlite3.Row型が返る
def read_by_Row_to_dict():
with closing(sqlite3.connect(DB)) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(SQL)
return [dict(row) for row in cursor] #dictが返る
def read_by_plain_to_ntp():
with closing(sqlite3.connect(DB)) as conn:
cursor = conn.cursor()
cursor.execute(SQL)
RowNamedtuple = namedtuple("RowNamedtuple", [field[0] for field in cursor.description])
return [RowNamedtuple._make(row) for row in cursor] #namedtupleが返る
それぞれ、
- 特に設定せずに、プレーンな
tuple
を取得する -
sqlite3.Row
型を取得する -
dict
にして取得する -
namedtuple
にして取得する
という場面を想定しています。
sqlite3.Row
についてはあとで説明します。
計測結果は以下の通りです。%timeitで10回ループしました。
%timeit -r 10 read_by_plain()
1 loop, best of 10: 96.7 ms per loop
%timeit -r 10 read_by_Row()
1 loop, best of 10: 100 ms per loop
%timeit -r 10 read_by_Row_to_dict()
1 loop, best of 10: 150 ms per loop
%timeit -r 10 read_by_plain_to_ntp()
1 loop, best of 10: 106 ms per loop
Row
はほとんどオーバーヘッドが無いようです。そして、namedtuple
も大差無い速度になっています。
対して、dict
はかなり差が付きました。
ただし、前記のようにキーアクセスの速度はdict
のほうが早いですので、パフォーマンスについては一長一短があります。
個人的には、格納にウェートがある場合は、namedtuple
のほうが扱いやすいです。たびたび編集するような用途ならdict
のほうがもちろん便利です。
補足:sqlite3.Rowについて
sqlite3.Row
というのはsqlite専用のデータ型です。これをconn.row_factory
に渡しておくと、読み出し結果がsqlite3.Row
で返ってきます。
sqlite3.Row
は高度に最適化されているとのことで、パフォーマンスは確かに良好です。更に、キーでもインデックスでもアクセスできますの、namedtuple
と似たところがあります。
単に読めればよいという場合、sqlite3.Row
だけでもよいかもしれません。
ただし、機能は限定されていますし、ドットアクセスもできません。私の感覚では、少し物足りない感じはあります。
たとえば、namedtuple
は_replace
ができるあたりなど嬉しいですね。
詳しくは
ドキュメント row_factory
ドキュメント sqlite3.Row
を参照してください。
最後に
namedtuple
にはなんだか魅力を感じます。もう少し使い方を研究してみたいです。
なお、投稿者は完全独学プログラマーのため、内容とか、訳とか、言葉使いとか色々間違っているところがあるかもしれません。遠慮なくご指摘いただければ幸いです。
参考情報
いくつかnamedtuple
の関連リンクを挙げておきます。
Pythonのdictをnamedtupleに変換してベンチ取ってみた
※ベンチマークがあります。