Pythonで構造体(のようなもの)が作れるか試してみた – NamedTuple使用

はじめに

沢山の資料が張られたホワイトボードを眺める男性

Pythonを使用したPJ(プロジェクト)に関わってる中で、そこそこ量の構造化された情報を操作する場合がある。
多次元配列(list,tuple,dict)で対処することも可能であるが、静的型付け言語の構造体のように型補完しやすい状態で扱いたい。

Pythonでは3.5から型アノテーションが導入されており、
以下の条件でできないか、調査してみた。

  • 多次元の構造で型補完ができるか
  • 今回の構造体はほぼ、定数的な扱いなので、再代入不可にできるか
  • 構造体を繰り返し処理できるか

使用したIDE、バージョン

  • Visual Studio Code
  • Python 3.9.6

ソースコード

結論 – NamedTupleを使用 –

typing moduleのNamedTupleを使用することで、
上記を全て満たす構造体を実現することができた。
NamedTupleとは名前付きタプルとのこと。
つまり再代入不可でかつパラメータを指定することもでき、
さらにこのモジュールは型アノテーションも可能である。

試行錯誤をいくつか施したので、
うまくいかなかったパターンも結論のあとに掲載を行う。

from datetime import date
from typing import NamedTuple
 
 
class Hand(NamedTuple):
   name       : str
 
class Arm(NamedTuple):
   name       : str
   hand       : Hand
 
class Foot(NamedTuple):
   name       : str
 
class Human(NamedTuple):
   name       : str
   birth_date : date = date(2017,5,1) # フィールド設定時に代入することで初期値の設定可能
   right_arm  : Arm  = Arm('伝説の右腕',Hand('伝説の右手'))
   left_arm   : Arm  = Arm(name='黄金の左腕',hand=Hand('黄金の左手'))
   right_foot : Foot = Foot('神が愛した右足')
   left_foot  : Foot = Foot(name='世界が泣いた左足')
 
 
 
if __name__ == '__main__':
   legend_human = Human('伝説の人類')
   print('--- index指定と名前指定で呼び出しできるか ---')
   print('名前',legend_human[0], legend_human.name)
   print('誕生日',legend_human[1], legend_human.birth_date)
   print('右腕',legend_human[2][0], legend_human.right_arm.name)
   print('左手',legend_human[3][1][0], legend_human.left_arm.hand.name)
 
   print('--- 再代入するとエラー検知されるか ---')
   try:
       legend_human.name = '伝説の再代入'
       print('失敗:再代入に成功しました')
   except Exception as e:
       print(e.__doc__)
 
   print('--- 繰り返し処理ができるか ---')
   for parts in legend_human:
       print(parts)

補完も以下のようにlegend_human.right_armと階層化されていても可能である

補完に成功しているスクリーンショット

そして、実行結果も想定した通り出力された。

% python3 named_tuple.py 
--- index指定と名前指定で呼び出しできるか ---
名前 伝説の人類 伝説の人類
誕生日 2017-05-01 2017-05-01
右腕 伝説の右手 伝説の右手
左手 黄金の左手 黄金の左手
--- 再代入するとエラー検知されるか ---
Attribute not found.
--- 繰り返し処理ができるか ---
伝説の人類
2017-05-01
Arm(name='伝説の右手', hand=Hand(name='伝説の右手'))
Arm(name='黄金の左腕', hand=Hand(name='黄金の左手'))
Foot(name='神が愛した右足')
Foot(name='世界が泣いた左足')
 

NamedTupleの書き方

公式ドキュメント

  • NamedTupleを継承したclassを作成する
  • フィールドにフィールド名と型をつける
from datetime import date
from typing import NamedTuple
 
class Human(NamedTuple):
   id: int
   name: str
   birth_date: date = date(2017,5,1) # フィールド設定時の代入でデフォルト値の設定可能

失敗したパターン

今回の構造体のようなものが作れるかの取り組みは当初は
collectionsのnamedtupleで実装できるか模索していたが、
動作はするものの、型判定により補完が効かなかったので、断念した。

公式ドキュメント

from datetime import date
from collections import namedtuple
 
Hand = namedtuple('Hand',('name'))
Arm  = namedtuple('Arm',('name','hand'))
Foot = namedtuple('Foot',('name'))
Human = namedtuple('Human',
('name','birth_date','right_arm','left_arm','right_foot','left_foot')
)
 
if __name__ == '__main__':
   right_hand = Hand('伝説の右手')
   left_hand  = Hand('黄金の左手')
   right_arm  = Arm('伝説の右手',right_hand)
   left_arm   = Arm('黄金の左腕',left_hand)
   right_foot = Foot('神が愛した右足')
   left_foot  = Foot('世界が泣いた左足')
 
   legend_human = Human(
       '伝説の人類',date(2017,5,1),right_arm,left_arm,right_foot,left_foot)
   print('--- index指定と名前指定で呼び出しできるか ---')
   print('名前',legend_human[0], legend_human.name)
   print('誕生日',legend_human[1], legend_human.birth_date)
   print('右腕',legend_human[2][0], legend_human.right_arm.name)
   print('左手',legend_human[3][1][0], legend_human.left_arm.hand.name)
 
   print('--- 再代入するとエラー検知されるか ---')
   try:
       legend_human.name = '伝説の再代入'
       print('失敗:再代入に成功しました')
   except Exception as e:
       print(e.__doc__)
 
   print('--- 繰り返し処理ができるか ---')
   for parts in legend_human:
       print(parts)

終わりに

調べてみたら案外typingモジュールのNamedTupleの情報が少なくて驚きました。
補完が効くのでコーディング中の情報量が多くて脳内記憶で対応できない時に重宝しています。

Pythonに関連するその他記事はこちら

作者情報

バックエンドエンジニア。Sier企画提案営業6年。独学1年を経て営業とエンジニアの職種並行期間1年。正式にエンジニアになり1年半経ちました。社内のIT教育推進担当としても邁進中。自身の開発環境のモダン化に挑戦中。ITに生きITに死す、我が人生。
公式Twitter