本記事では、Pythonで構造体を扱う方法を解説します。Pythonには構造体がありませんが、dataclassを用いると構造体のようなことができます。そのため、dataclassの使い方を解説します。
構造体とは
構造体とは、複数の異なる型のデータを1つにまとめたデータ構造です。多くのプログラミング言語(C、C++、Rustなど)では組み込み機能として提供されていますが、Pythonには直接的な構造体の機能はありません。
例えば、C言語での構造体は以下のように定義します。
struct Person {
char name[50];
int age;
float height;
};
構造体を使うメリットとしては、関連するデータをまとめて扱うことができ、コードの可読性と保守性が向上することです。また、関数に複数の値をまとめて渡すことが容易になります。
dataclassとは
Pythonでは、構造体の代わりにdataclassを使用できます。dataclassはPython 3.7から標準ライブラリに追加された機能で、データを格納するためのクラスを簡潔に定義できます。
基本構文
dataclassの基本構文は以下の通りです。
from dataclasses import dataclass
@dataclass # デコレータとして@dataclassを使用
class クラス名:
フィールド名1: 型ヒント = デフォルト値(省略可)
フィールド名2: 型ヒント
フィールド名3: 型ヒント = デフォルト値(省略可)
主な要素を説明します。
@dataclass
– クラスをデータクラスとして定義するデコレータフィールド名: 型ヒント
– クラスが持つフィールドとその型を宣言= デフォルト値
– 任意でフィールドのデフォルト値を設定可能
実際の使用例は以下のとおりです。
from dataclasses import dataclass
@dataclass
class Person:
name: str # 名前(文字列型)
age: int # 年齢(整数型)
height: float = 0.0 # 身長(浮動小数点型、デフォルト値は0.0)
# 使用例
person = Person("山田太郎", 30, 175.5)
print(person.name) # 山田太郎
print(person.age) # 30
print(person) # Person(name='山田太郎', age=30, height=175.5)
dataclassを使うメリットは以下の点です。
- コード量の削減: 通常のクラスでは、
__init__
や__repr__
などのメソッドを手動で実装する必要がありますが、dataclassではこれらが自動生成されるため、短いコードで済みます。 - 特殊メソッドの自動生成: 以下のメソッドが自動的に生成されます。
__init__
: インスタンス初期化__repr__
: デバッグ用の文字列表現__eq__
: 等価性比較(==演算子)__hash__
: ハッシュ値の計算(frozenが有効な場合)
- 型安全性の向上: 型ヒントを使用することで、コード実行前にIDEやmypyなどの静的型チェッカーがエラーを検出できるため、バグを早期に発見できます。
- データの意図の明確化: フィールドの型を明示することで、そのクラスが扱うデータの種類が一目でわかり、コードの可読性が向上します。
- イミュータブル(変更不可)なオブジェクトの簡単な作成:
frozen=True
パラメータを使用することで、一度作成したらフィールド値を変更できないオブジェクトを簡単に作成できます。
例
基本的な使い方
以下は学生情報を扱うdataclassの基本的な例です。名前、学生ID、成績を持つStudentクラスを定義し、インスタンスを作成して情報を表示しています。
from dataclasses import dataclass
@dataclass
class Student:
name: str # 学生の名前
id: int # 学生ID
grade: float # 成績
# 学生インスタンスの作成
student1 = Student("佐藤花子", 101, 85.5)
student2 = Student("鈴木一郎", 102, 92.0)
print(student1) # Student(name='佐藤花子', id=101, grade=85.5)
print(student2) # Student(name='鈴木一郎', id=102, grade=92.0)
# フィールドへのアクセス
print(f"{student1.name}の成績は{student1.grade}点です") # 佐藤花子の成績は85.5点です
デフォルト値の設定
このサンプルコードはアプリケーションの設定を保存するdataclassを定義しています。各設定項目にはデフォルト値が設定されており、必要な項目だけを上書きすることができます。
from dataclasses import dataclass
@dataclass
class Configuration:
host: str = "localhost" # ホスト名(デフォルトはlocalhost)
port: int = 8000 # ポート番号(デフォルトは8000)
debug: bool = False # デバッグモードの有無
max_connections: int = 100 # 最大接続数
# デフォルト値をそのまま使用した設定
default_config = Configuration()
print(default_config) # Configuration(host='localhost', port=8000, debug=False, max_connections=100)
# 一部のフィールドだけ指定してカスタム設定を作成
custom_config = Configuration(host="example.com", port=443)
print(custom_config) # Configuration(host='example.com', port=443, debug=False, max_connections=100)
# デバッグモードをオンにした設定
debug_config = Configuration(debug=True)
print(debug_config) # Configuration(host='localhost', port=8000, debug=True, max_connections=100)
比較と等価性
このサンプルコードは、dataclassで定義した座標オブジェクト(Point)の比較機能を示しています。dataclassでは等価性比較(==)が自動的に実装されるため、全てのフィールドの値が等しいオブジェクト同士は等価と判定されます。
from dataclasses import dataclass
@dataclass
class Point:
x: int # X座標
y: int # Y座標
# 3つの座標点を作成
p1 = Point(5, 10)
p2 = Point(5, 10) # p1と同じ値
p3 = Point(3, 7) # 異なる値
# 等価性を比較
print(p1 == p2) # True(同じ値を持つため等価)
print(p1 == p3) # False(異なる値)
print(p1) # Point(x=5, y=10)
# リストに入れて使用する例
points = [p1, p3]
print(p2 in points) # True(p2はp1と等価なのでリストに存在すると判定される)
イミュータブル(変更不可)なdataclass
このサンプルコードは変更できない定数を表すdataclassを定義しています。frozen=True
オプションを使用することで、インスタンス作成後にフィールド値を変更しようとするとエラーが発生します。
from dataclasses import dataclass
@dataclass(frozen=True) # frozenオプションで変更不可能に設定
class Constants:
PI: float = 3.14159 # 円周率
GRAVITY: float = 9.8 # 重力加速度
# 定数オブジェクトを作成
constants = Constants()
print(constants.PI) # 3.14159
# 値の変更を試みる(実行時エラーが発生)
try:
constants.PI = 3.14 # FrozenInstanceError: cannot assign to field 'PI'
except Exception as e:
print(f"エラー: {e}") # エラー: cannot assign to field 'PI'
# 異なる値を持つ新しいインスタンスは作成可能
other_constants = Constants(PI=3.14)
print(other_constants.PI) # 3.14
注意点
ミュータブルなデフォルト値の扱い
このサンプルコードは、リストなどの変更可能なオブジェクトをデフォルト値に設定する際の問題と、正しい使用方法を示しています。単純にリストをデフォルト値として使うと、全てのインスタンス間でそのオブジェクトが共有されて予期せぬ動作を引き起こします。
from dataclasses import dataclass, field
# 誤った例:変更可能なオブジェクトを直接デフォルト値にする
@dataclass
class BadExample:
name: str
values: list = [] # 全てのインスタンスで同じリストが共有されてしまう!
# 正しい例:default_factoryを使用する
@dataclass
class GoodExample:
name: str
values: list = field(default_factory=list) # 各インスタンスに独自のリストが作成される
# 問題を実演
bad1 = BadExample("example1")
bad2 = BadExample("example2")
bad1.values.append(1) # bad1のリストに要素を追加
print(f"bad1.values: {bad1.values}") # bad1.values: [1]
print(f"bad2.values: {bad2.values}") # bad2.values: [1] - bad1で追加した値がbad2にも影響してしまう
# 正しい実装の動作
good1 = GoodExample("example1")
good2 = GoodExample("example2")
good1.values.append(1) # good1のリストに要素を追加
print(f"good1.values: {good1.values}") # good1.values: [1]
print(f"good2.values: {good2.values}") # good2.values: [] - good2は空のリストのまま
継承時の振る舞い
このサンプルコードはdataclassの継承の例を示しています。子クラスは親クラスのフィールドを全て引き継ぎ、新しいフィールドを追加することができます。
from dataclasses import dataclass
# 基本となる人物クラス
@dataclass
class Person:
name: str # 名前
age: int # 年齢
# Personを継承した従業員クラス
@dataclass
class Employee(Person):
employee_id: str # 社員ID
department: str = "一般" # 部署(デフォルト値あり)
# 従業員インスタンスの作成
# Person(親クラス)のフィールドと、Employee(子クラス)のフィールドの両方を指定
emp = Employee("田中次郎", 35, "E001")
# 全てのフィールドにアクセス可能
print(emp) # Employee(name='田中次郎', age=35, employee_id='E001', department='一般')
print(f"名前: {emp.name}, 部署: {emp.department}") # 名前: 田中次郎, 部署: 一般
# 部署を指定したインスタンス
manager = Employee("佐藤部長", 45, "M001", "管理")
print(manager) # Employee(name='佐藤部長', age=45, employee_id='M001', department='管理')
まとめ
Pythonではdataclassを使用することで、構造体のような機能を実現できます。dataclassを使うと以下のメリットがあります。
- 少ないコードでデータを格納するクラスを定義できる
- 特殊メソッドが自動生成されるため、開発効率が向上する
- 型ヒントによりコードの可読性が高まる
- イミュータブルなオブジェクトも簡単に作成できる
Python 3.7以降のプロジェクトでは、データを扱うクラスにはdataclassを積極的に活用すると良いでしょう。特に複数のフィールドを持つデータ構造を扱う場合に威力を発揮します。