LoginSignup
14
3

More than 1 year has passed since last update.

【Python】型付きのNamedTupleはいいぞ

Last updated at Posted at 2022-12-02

この記事は、株式会社ディーバ 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を返している訳ではありません)
生成したサブクラスを用いて「名前付きのタプル」を生成しています。

namedtupletupleのサブクラスなので、基本的には普通のタプルと同じ扱いが出来ます。
イミュータブル型なので変更できませんし、アンパック代入も出来ます。

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型の表示となる(→ 何の型が入るべきなのか調査が必要になる)
image.png

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を継承したクラスを作成し、引数に型アノテーションを付けるだけで完成です。

マウスオーバーした際の型情報も適切に表示されます。
image.png

上記で少し触れた通り、型アノテーションには強制力はありませんので、型とは異なる値を入れることは可能です。

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()
一応出来なくはないって話

dataclassastuple()でタプル変換すればアンパック代入することが出来ます。

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というものがある」ということを頭の片隅に置いていただければ幸いです。

14
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
3