🫔

Pythonで型定義にNamedTupleを使うのはやめたほうが良さそう

2024/01/10に公開

はじめに

pythonでもある程度しっかり型定義をしたくてTypedDictやdataclassを調べていました。
その過程で「NamedTupleでも行けるんじゃね?」と思ったのですが、ダメそうな挙動になったのでそのメモです。

TypedDictやdataclassによる型定義の概要

pythonで型を定義したいとき、TypedDictやdataclassを使うと次のように書くことができます。この2つの違いは一旦おいておきますが、どちらを使ってもPythonで独自の型を定義して使うことができます。

TypedDict

from typing import TypedDict

class TD1(TypedDict):
    a: str
    b: int

td1: TD1 = TD1(a='z', b=0)
print(td1)
output
{'a': 'z', 'b': 0}

dataclass

from dataclasses import dataclass

@dataclass
class DC1:
    a: str
    b: int

dc1: DC1 = DC1(a='z', b=0)
print(dc1)
output
DC1(a='z', b=0)

NamedTupleで型定義してみたらどうなるか

とりあえず定義してみる

from typing import NamedTuple

class NT1(NamedTuple):
    a: str
    b: int

nt1: NT1 = NT1(a='z', b=0)
print(nt1)
output
NT1(a='z', b=0)

TypedDictやdataclassと全く同じで問題なさそうに見えます。

しかし、、、

NamedTuple同士を比較してみる

次のコードと実行結果をごらんください。

from typing import NamedTuple

class NT1(NamedTuple):
    a: str
    b: int

class NT2(NamedTuple):
    a: str
    b: int

class NT3(NamedTuple):
    b: int
    a: str

class NT4(NamedTuple):
    c: str
    d: int

nt1: NT1 = NT1(a='z', b=0)
nt2: NT2 = NT2(a='z', b=0)
nt3: NT3 = NT3(a='z', b=0)
nt4: NT4 = NT4(c='z', d=0)

print('nt1 == nt2 : ', nt1 == nt2)
print('nt1 == nt3 : ', nt1 == nt3)
print('nt1 == nt4 : ', nt1 == nt4)
output
nt1 == nt2 :  True
nt1 == nt3 :  False
nt1 == nt4 :  True

おかしいことにお気づきでしょうか。

  • nt1nt2が同じものと判定されています。
    • クラスが違うので違和感を覚える人もいるかも知れませんが、中身が完全に一致いるので同じモノということで理解出来るかと思います。
  • nt1nt3は中身が同じなのにFalseになっています。
    • さっきと結果が違うじゃないかと思ってしまいます。しかしよく見てください。NT1NT3abの順番が違います。なので別物です。
  • nt1nt4は、急にcdが出てくるので別物にしか見えません。しかし、同じものとして判定されます。
    • 何故かというと両方とも1番目の要素がzで2番目の要素が0だからです。

これはどういうことかというと、NamedTupleはあくまでtupleであり、比較の際named部分は見ずにtuple部分のみで比較しているということになります。[1][2]

応用(?)事例

これを応用すると、正しいけどヤバいコードを書くことができます。

from typing import NamedTuple

class User(NamedTuple):
    first_name: str
    last_name: str
    age: int
    email: str = ""

class Name(NamedTuple):
    last_name: str
    first_name: str

name = Name(first_name='太郎', last_name='山田')
user = User(*name, age=20)
print(user)
output
User(first_name='山田', last_name='太郎', age=20, email='')

名字と名前を逆になりました。
これはtupleの挙動として正しいですので、mypyの型チェックに引っかかりませんでした。
私はコレをコードレビューで見つけられる自信がありませんので、NamedTupleの利用は気をつけたほうが良いと思います。

型付けに使うなら、やっぱりTypedDictかdataclassが良さそうですね。

(おまけ)AIに出力結果を聞いてみた

全員間違えました!!

Duet AI

Amazon Q

Copilot Chat

脚注
  1. 同じことをdataclassでやるとFalse,False,False、TypedDictでやるとTrue,True,Falseになります。それぞれの結果は異なりますが、dataclassは「公称型」、TypedDictは「構造的部分型」の型システムであると見ることができ、dataclassやTypedDictは問題なく型定義として使用できます。 ↩︎

  2. 海外の記事などを見てるとこの手の話題には必ずと言っていいほど「__eq__がなんちゃらかんちゃら」とケチを付けいる人を見かけるので予め言っておきます。細かいことはあえて無視しているんだよ分かりにくいから。 ↩︎

NCDCエンジニアブログ

Discussion