Python: named tupleについて

Python

tupleを見てきたが、その流れでnamed tupleを調べたので備忘録として。

list, tuple, dictionaryはindexかkeyでデータを選ぶ

list、tupleは基本的にindexで内包データを管理(参照(出力)、上書き(tupleは参照のみ))する。

dictionaryは基本的にkeyを元にデータを管理する。

# listはindexでデータを管理
>>> list1 = [1, 2, 3, 4, 5, 6]
>>> list1[3] # index が3 =前から4つめのデータを出力
4

>>> list1[4] = 10 # indexが4 =前から5つめのデータを10に上書き
>>> list1
[1, 2, 3, 4, 10, 6]

# dictionaryはKeyでデータを管理
>>> dic1 = {"a":1, "b":2, "c":3, "d":4, "e":5, "f":6}
>>> dic1["c"] # Keyが"c"のデータを出力
3

>>> dic1["e"] = 10 # Keyが"e"のデータを10に上書き
>>> dic1
{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 10, 'f': 6}

listやtupleではKeyでデータの管理は出来ないし、dictionaryはindexでデータの管理は出来ない。

しかし、indexと名前を両方使って管理できるデータタイプがある。

それがnamed tuple。

collections.namedtuple

データの作り方

named tupleを使用するには通常より2手順必要。

モジュールcollectionsからnamedtupleをimportする手順と、class(下記ではntpl1 class)を作る手順。

classを作る手順でdictionaryのkeyに相当するname(下記では”a”, “b”〜)を設定する。これはclassを作る際に良く言われる設計図(blue print)。

新しい変数(下記では”alp”)にclassとそのvalueを代入する事でnamedtupleのデータが出来る。

>>> from collections import namedtuple
>>> ntpl1 = namedtuple("ntpl1", ["a", "b", "c", "d", "e", "f"])
>>> ntpl1
<class '__main__.ntpl1'>
>>> type(ntpl1)
<class 'type'>

>>> alp = ntpl1(1, 2, 3, 4, 5, 6)
>>> alp
ntpl1(a=1, b=2, c=3, d=4, e=5, f=6)

>>> alp[3] # indexでデータを得る場合
4
>>> alp.e # nameでデータを得る場合
5

alp[3]は場所(index)でデータを得、alp.eはnameでデータを得ている。

namedtupleはtuple/class/dictionaryの混合(hybrid)と言える。

nameでもindexでもデータを扱える様になり、nameがある分コードが読み易くなる、便利な仕組みと言える。

unpacking

namedtupleの要素数と同じ数の変数にそれぞれデータを放り込むのがunpacking。

>>> alp
ntpl1(a=1, b=2, c=3, d=4, e=5, f=6)

>>> s, t, u, v, w, x = alp # s~xの6つの変数にunpacking

>>> s, t, u, v, w, x
(1, 2, 3, 4, 5, 6)

データの加工

dictionaryやlistの様にデータが加工できるだろうか?

>>> alp[4] = 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'ntpl1' object does not support item assignment

>>> alp.e = 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

データの加工は受け付けないようだ。

メソッドを見てみる。

>>> dir(alp)
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__module__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '_asdict', '_field_defaults', '_fields', '_make', '_replace', 'a', 'b', 'c', 'count', 'd', 'e', 'f', 'index']

a~fは作った時に設定したkeyとしてのname、こう見ると、tupleの流れだけに、基本的にcount、indexしかメソッドが無いのが分かる。

alpを新規代入する事によるデータの変更は可能だが、下記を見て分かる通り、最初のalpと新規で作ったalpのidは別のもの。別のnamedtupleが作られた、と言う事が分かる。

>>> id(alp)
4425727136
>>> alp = ntpl1(1, 2, 3, 4, 10, "b")
>>> alp
ntpl1(a=1, b=2, c=3, d=4, e=10, f='b')
>>> id(alp)
4425726560

dictionaryに置き換える

named tupleはデータの形がkey:valueペアなので、”_asdict()”で新しいdictionary型データを作る事ができる。

>>> A = alp._asdict() # dictionary型としてAに代入
>>> A
{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6}
>>> type(A)
<class 'dict'>

>>> A["c"]
3
>>> A["a"]
1

>>> A["b"] = 10 # dictionary型なのでmutable、上書きも可能
>>> A
{'a': 1, 'b': 10, 'c': 3, 'd': 4, 'e': 5, 'f': 6}

typing.NamedTuple

こちらはcollections.namedtuple の型付き版。python3.6から使える。

データの型書く事ができる。

作り方

typingからNamedTupleをインポートして、class “Alp”を作る。

alpはstr型、numはint型と設定、__repr__で出力時の書式を設定する。

>>> from typing import NamedTuple
>>> class Alp(NamedTuple):
...     alp:str
...     num:int
...
...     def __repr__(self) -> str:
...             return f'<Alp {self.alp}, num={self.num}>'

>>> alp = Alp("a", 1)
>>> alp
<Alp a, num=1>

>>> alp.__repr__
<bound method Alp.__repr__ of <Alp a, num=1>>

# indexでデータを取得
>>> alp[0]
'a'
>>> alp[1]
1

# nameでデータを取得
>>> alp.alp
'b'
>>> alp.num
2

Alp.alp(= alp.alp)に”a”、Alp.num(= alp.num)に1を入力。

indexでも、nameでもデータを得ることができる。

新しいデータを入れる(2022/6/6 タイトルを追記)

別のデータを入れる事もできるが、tupleなので新しいデータを作っている。

“a”, 1のalpと”b”, 1のalpはidが異なっているのが分かる。

>>> alp = Alp("a", 1)
>>> id(alp)
4494315968
>>> alp
<Alp a, num=1>

>>> alp = Alp("b", 2)
>>> id(alp)
4491927872
>>> alp
<Alp b, num=2>

注意:collections.namedtupleとの違い(当項目全文追記 2022/6/6)

ちなみに、「作り方」の項目でのコードにおいて、最初の

>>> class Alp(NamedTuple):
...     alp:str
...     num:int

の部分はcollections.namedtupleの以下と等価(equivalent)と言う理解。

Alp = collections.namedtuple("Alp", ["a"])

ここでcollections.namedtupleの時のように

Alp = collections.namedtuple("Alp", ["a", "b", "c", "d", "e", "f"])

とはしていない事に注意。

上記「新しいデータを入れる」の箇所で書いた通り、新しいname(Key的なもの)、valueを入れると、新しいデータが作られるだけで、dictionary化をしてもその時の組み合わせがdictionaryになるのみ。

>>> alp
<Alp a, num=1> # 最初のalp

>>> alp = Alp("c", 3) # alp = "c"、num = 3を入れた新しいalpを作る。
>>> A = alp._asdict() # alpをdictionary化する
>>> A
{'alp': 'c', 'num': 3} # 最初のa, 1は出て来ず、_asdict()を実行したときのalpが出てくる。

現時点でtyping.NamedTupleでこの様な多数のname(key)を扱う方法は理解できていない。

(この項2022/6/6 追記)

型設定について

型を設定できるとはいえ、制限を掛けられる訳ではなく、strとした箇所にintデータ4を入れても、intとした所にstrデータ”d”を入れても問題なく入る。

>>> alp2 = Alp(4, "d")
>>> alp2
<Alp 4, num=d>

typing.NamedTupleにおける型設定は、コードの読み易さの為の機能、と言う理解。

参考文献

この記事は以下の情報を参考にして執筆しました。

[Learning Python, 5th Edition] Mark Lutz, O’Reilly Media (amazonのページはこちら)
[Python Tricks: A Buffet of Awesome Python Features] Dan Bader ←amazonのページにリンクしています
Python 公式チュートリアル

コメント

タイトルとURLをコピーしました