中学英語808712 views
いろは2986023 views
高校日本史189857 views
ヒストリア284143 views
教育148875 views
中学社会667106 views
高校物理158224 views
高校国語785655 views
数学講師2852771 views
MathPython491378 views
Help
Tools

English

make_dataclass で動的にクラスを生成する|Python

dataclass は通常、デコレータを使ってクラス定義時に静的に作成する。しかし、フィールド構成が実行時まで確定しない場面もある。CSV のヘッダーから自動でクラスを作りたい、設定ファイルに応じてフィールドを変えたい、といったケースだ。make_dataclass はそうした動的なクラス生成を可能にする関数である。

make_dataclass の基本

dataclasses.make_dataclass は、クラス名とフィールドのリストを受け取り、dataclass を返す。

from dataclasses import make_dataclass

Point = make_dataclass("Point", ["x", "y"])

p = Point(x=1.0, y=2.0)
print(p)       # Point(x=1.0, y=2.0)
print(p.x)     # 1.0

フィールドをリストで渡すだけで、@dataclass を付けたクラスと同等のものが生成される。__init____repr____eq__ などのメソッドもすべて自動で作られる。

これは以下のコードとほぼ同じ結果になる。

from dataclasses import dataclass

@dataclass
class Point:
    x: ...
    y: ...

違いは、make_dataclass ではクラス名もフィールドも文字列として渡せる点だ。つまり、プログラムの実行中に決まる情報からクラスを組み立てられる。

フィールドに型を指定する

フィールド名だけを渡すと型は typing.Any になる。型を明示するには、タプルで (名前, 型) を渡す。

from dataclasses import make_dataclass

User = make_dataclass("User", [
    ("name", str),
    ("age", int),
    ("score", float),
])

u = User(name="Alice", age=30, score=95.5)
print(u)  # User(name='Alice', age=30, score=95.5)

型ヒントとして記録されるだけで、実行時の型チェックが走るわけではない。これは通常の dataclass と同じ挙動だ。

デフォルト値を設定する

3 要素のタプル (名前, 型, デフォルト値) でデフォルト値を指定できる。field() も使える。

from dataclasses import make_dataclass, field

Config = make_dataclass("Config", [
    ("host", str, "localhost"),
    ("port", int, 8080),
    ("tags", list, field(default_factory=list)),
])

c = Config()
print(c)  # Config(host='localhost', port=8080, tags=[])

通常の dataclass と同じく、デフォルト値のないフィールドはデフォルト値のあるフィールドより前に置かなければならない。順序を間違えると TypeError になる。

from dataclasses import make_dataclass

# これはエラーになる
Bad = make_dataclass("Bad", [
    ("name", str, "default"),  # デフォルトあり
    ("age", int),              # デフォルトなし → TypeError
])

オプションの指定

make_dataclass@dataclass デコレータと同じオプションを受け取れる。frozenslotsorder などをキーワード引数で渡す。

from dataclasses import make_dataclass

FrozenPoint = make_dataclass(
    "FrozenPoint",
    [("x", float), ("y", float)],
    frozen=True,
    slots=True,
)

p = FrozenPoint(x=1.0, y=2.0)
print(hash(p))   # ハッシュ可能
# p.x = 3.0      # FrozenInstanceError
@dataclass デコレータ

ソースコード上でクラスを定義する。フィールド構成はコードに固定される。可読性が高く、IDE の補完も効く。

make_dataclass 関数

実行時にクラス名とフィールドを組み立てる。動的な生成が可能だが、IDE の型補完は効きにくい。

静的に定義できるなら @dataclass を使うべきだ。make_dataclass は「静的に書けない場面」のためのツールである。

CSV ヘッダーからクラスを生成する

make_dataclass の実用的な使い方として、CSV ファイルのヘッダーからデータクラスを自動生成するパターンがある。

import csv
from dataclasses import make_dataclass

csv_text = """name,age,city
Alice,30,Tokyo
Bob,25,Osaka
Charlie,35,Nagoya"""

reader = csv.reader(csv_text.strip().splitlines())
headers = next(reader)

Row = make_dataclass("Row", [(h, str) for h in headers])

rows = [Row(*values) for values in reader]
for row in rows:
    print(row)

# Row(name='Alice', age='30', city='Tokyo')
# Row(name='Bob', age='25', city='Osaka')
# Row(name='Charlie', age='35', city='Nagoya')

ヘッダーが変わってもコードを書き換える必要がない。列の追加や削除に自動で対応できるため、汎用的なデータ読み込みツールに向いている。

辞書からクラスを生成する

API レスポンスや設定ファイルの辞書から dataclass を作るパターンも有用だ。

from dataclasses import make_dataclass

def dict_to_dataclass(name, data):
    fields = [(k, type(v), v) for k, v in data.items()]
    cls = make_dataclass(name, fields)
    return cls

schema = {"host": "localhost", "port": 8080, "debug": False}
Config = dict_to_dataclass("Config", schema)

c = Config()
print(c)  # Config(host='localhost', port=8080, debug=False)

c2 = Config(host="0.0.0.0", port=3000, debug=True)
print(c2)  # Config(host='0.0.0.0', port=3000, debug=True)

辞書の値から型を推論し、同時にデフォルト値として設定している。簡易的なアプローチだが、設定ファイルのパースなどでは十分に実用的だ。

基底クラスの指定

bases 引数で親クラスを指定できる。既存のクラスを継承した dataclass を動的に作れる。

from dataclasses import make_dataclass, dataclass

@dataclass
class Base:
    id: int

Extended = make_dataclass(
    "Extended",
    [("name", str), ("value", float)],
    bases=(Base,),
)

e = Extended(id=1, name="test", value=3.14)
print(e)              # Extended(id=1, name='test', value=3.14)
print(isinstance(e, Base))  # True

既存の型階層に動的なクラスを組み込めるため、プラグインシステムやスキーマ駆動のコード生成で活用できる。

名前空間にメソッドを追加する

namespace 引数を使うと、生成されるクラスにメソッドやプロパティを追加できる。

from dataclasses import make_dataclass
import math

Circle = make_dataclass(
    "Circle",
    [("radius", float)],
    namespace={
        "area": property(lambda self: math.pi * self.radius ** 2),
        "circumference": property(lambda self: 2 * math.pi * self.radius),
    },
)

c = Circle(radius=5.0)
print(f"面積: {c.area:.2f}")      # 面積: 78.54
print(f"周長: {c.circumference:.2f}")  # 周長: 31.42

ただし、namespace に多数のメソッドを詰め込むなら、通常の @dataclass で静的にクラスを定義するほうが可読性は高い。namespace は補助的な用途に留めるのが賢明だ。

make_dataclass でフィールドにデフォルト値を設定するとき、正しい書き方はどれか?

  • ("name", "default", str)
  • ("name", str, "default")
  • {"name": str, "default": "value"}
  • field("name", default="value")
__RESULT__

フィールドは (名前, 型, デフォルト値) の 3 要素タプルで指定します。順番は名前 → 型 → デフォルト値です。

make_dataclass の限界

動的生成は強力だが、トレードオフも明確にある。

型チェッカーとの相性

mypy や pyright は make_dataclass で生成したクラスの型情報を追跡できない。静的解析の恩恵を受けられないため、大規模プロジェクトでは型安全性が低下する。

可読性の低下

どんなフィールドを持つクラスなのかがソースコードを読むだけではわからない。デバッグ時にクラス定義を追いかけるのが難しくなる。

IDE サポートの不足

コード補完やリファクタリング支援が効かない。属性名のタイポに気づきにくく、開発効率に影響する。

設計時にフィールドが確定しているなら、迷わず @dataclass を使うべきだ。make_dataclass は、外部データやメタ情報に基づいてクラスを生成する必要がある場面に限定して使うのが適切である。動的生成の便利さと引き換えに失うものを理解した上で、意図的に選択することが重要だ。