この記事は、株式会社ディーバ PD部 Advent Calendar 3日目 の記事になります。
namedtupleとは
読んで字の如く名前付きのタプルのことです。
通常のタプルでは、user[0]
のようにインデックスで値を取得します。
user[0]
だけでは、user
の先頭の要素を取得している以外の情報は分かりません。
# userの0番目には何が入っているのか調査が必要になる
hoge = user[0]
名前付きタプルでは、インデックスに名前を付けてuser.name
のようにドットアクセスで値を取得できます。
# 一目見るだけでユーザ名が入っていることが分かる
hoge = user.name
このようにnamedtuple
を用いると、可読性が向上するメリットがあります。
2種類のnamedtuple
namedtuple
には下記の2つが存在します。
collections.namedtuple
typing.NamedTuple
今回はこの2種類に触れつつ、typing.NamedTuple
の良さを知っていただければと思います。
collections.namedtuple
from collections import namedtuple
を使用します。
from collections import namedtuple
def main():
User = namedtuple("User", ["name", "age"])
user = User("Alice", 20)
print(f"{user.name=}, {user.age=}") # user.name=Alice, user.age=20
if __name__ == "__main__":
main()
定義方法は、第一引数にtuple
のサブクラス名、第二引数に属性名を定義します(詳細はドキュメントへ)
namedtuple
の返り値はtuple
のサブクラスを返しています(直接tupleを返している訳ではありません)
生成したサブクラスを用いて「名前付きのタプル」を生成しています。
namedtuple
はtuple
のサブクラスなので、基本的には普通のタプルと同じ扱いが出来ます。
イミュータブル型なので変更できませんし、アンパック代入も出来ます。
from collections import namedtuple
def main():
User = namedtuple("User", ["name", "age"])
user = User("Alice", 20)
user.name = "Bob" # AttributeError: can't set attribute(変更不可)
name, age = user # アンパック代入
print(name, age) # Alice 20
if __name__ == "__main__":
main()
collections.namedtupleの問題点
collections.namedtuple
には、型アノテーションを付けることが出来ません。
「Pythonで型を付けてもコメントみたいなもんだし意味ないのでは?」と思う方もいるかもしれません。
確かに強制力はありませんが、マウスオーバーしたときに型を示してくれるので、「ここには何の型が入るべきだっけ?」という調査が不要になります。
※型を付けていないとAny
型の表示となる(→ 何の型が入るべきなのか調査が必要になる)
namedtuple
で型を正しく表示させたいときに役立つのが、今回の本題であるtyping.NamedTuple
です。
typing.NamedTupleとは
collection.namedtuple
に型が付いたものです。
from typing import NamedTuple
を使用します。
from typing import NamedTuple
class User(NamedTuple):
name: str
age: int
def main():
user = User("Alice", 20)
print(user.name, user.age) # Alice 20
if __name__ == "__main__":
main()
これは、上記で紹介したnamedtuple("User", ["name", "age"])
と等価です。
NamedTuple
を継承したクラスを作成し、引数に型アノテーションを付けるだけで完成です。
上記で少し触れた通り、型アノテーションには強制力はありませんので、型とは異なる値を入れることは可能です。
def main():
user = User(20, "Alice")
print(user.name) # 20
if __name__ == "__main__":
main()
定義方法が変わってもnamedtuple
であることに変わりませんので、イミュータブル型ですし、アンパック代入も出来ます。
def main():
user = User("Alice", 20)
user.name = "Bob" # AttributeError: can't set attribute(変更不可)
name, age = user
print(name, age) # Alice 20
if __name__ == "__main__":
main()
注意点として、デフォルト値を付ける場合には、デフォルト値のないフィールドの後でないといけないというルールがあります。
# NG
class User(NamedTuple):
name: str = "Alice"
age: int
# OK
class User(NamedTuple):
age: int
name: str = "Alice"
@dataclass(frozen=True)
との違い
見た目や機能が近しいものに@dataclass(frozen=True)
があります。
@dataclass(frozen=True)
をざっくり説明すると、「楽にクラス定義が出来るdataclass
の機能」に、「変数をイミュータブル型に出来るfrozen=True
」を付けたデコレータです(ドキュメント)。
これとの大きな違いはtuple
であるかどうかになります。
@dataclass(frozen=True)
の方はtuple
ではありませんので、アンパック代入は出来ません。
from dataclasses import dataclass
@dataclass(frozen=True) # イミュータブル設定
class User:
name: str
age: int
def main():
user = User("Alice", 20)
print(user.name, user.age) # Alice 20
name, age = user # TypeError: cannot unpack non-iterable User object
if __name__ == "__main__":
main()
一応出来なくはないって話
dataclass
をastuple()
でタプル変換すればアンパック代入することが出来ます。
from dataclasses import dataclass, astuple
@dataclass(frozen=True)
class User:
name: str
age: int
def main():
user = User("Alice", 20)
name, age = astuple(user)
print(name, age) # Alice 20
if __name__ == "__main__":
main()
補足
namedtuple
を紹介しましたが、全てのタプルをnamedtupleにするべきということはありません。
スコープが狭かったり、変数名を見るだけでどこに何が格納されているものかがはっきり分かるものは不要だと自分は思います。
このあたりの裁量は開発者に依る部分が大きいですが、「namedtuple
というものがある」ということを頭の片隅に置いていただければ幸いです。