データを持つクラスの定型コードを減らす
取引、気配値、設定のように、いくつかのフィールドを持つことが主目的のクラスは多くあります。それぞれに __init__、__repr__、__eq__ を書くのは繰り返しになります。dataclasses モジュールの @dataclass デコレーターは、型注釈付きフィールドの一覧からそれらを自動で書いてくれます。symbol: str、price: float、quantity: int を一度宣言するだけで、コンストラクター Trade("AAPL", 184.25, 200)、Trade(symbol='AAPL', price=184.25, quantity=200) のような読みやすい repr、フィールド単位の等価比較が得られます。注釈は意図を示すもので、Python は強制しません。
from dataclasses import dataclass
@dataclass
class Trade:
symbol: str
price: float
quantity: int
a = Trade("AAPL", 184.25, 200)
b = Trade("AAPL", 184.25, 200)
print(a) # Trade(symbol='AAPL', price=184.25, quantity=200)
print(a == b) # True: compared field by field
print(a.price * a.quantity) # 36850.0デフォルト値、field()、変更可能な値
フィールドにはデフォルト値を付けられます(currency: str = "USD")。デフォルト付きのフィールドは、付いていないフィールドの後ろに置きます。リストのような変更可能なデフォルト値を単に = [] と書くと全オブジェクトで共有されてしまうため(Python は拒否します)、trades: list = field(default_factory=list) と書いて、オブジェクトごとに新しいリストを持たせます。field(...) では、フィールドを repr(repr=False)や比較(compare=False)から外すこともできます。
from dataclasses import dataclass, field
@dataclass
class Book:
name: str
currency: str = "USD" # a default
trades: list = field(default_factory=list) # a fresh list per object
one = Book("desk-a")
two = Book("desk-b")
one.trades.append(("AAPL", 100))
print(one) # Book(name='desk-a', currency='USD', trades=[('AAPL', 100)])
print(two.trades) # [] (not shared)
print(Book("x", "EUR")) # Book(name='x', currency='EUR', trades=[])独自のメソッド、__post_init__、代替コンストラクター
データクラスも普通のクラスなので、メソッドを追加できます。__post_init__(self) は生成された __init__ の直後に実行され、値の検証や整形(self.symbol = self.symbol.upper()、不正な価格には raise ValueError(...))を行う場所です。@classmethod は第1引数にクラスを cls として受け取り、テキストなど別の形からオブジェクトを作る定番の方法です。Trade.from_line("AAPL,184.25,200") は cls(symbol, float(price), int(quantity)) を返すので、検証も引き続き働き、サブクラスでも機能します。
from dataclasses import dataclass
@dataclass
class Trade:
symbol: str
price: float
quantity: int
def __post_init__(self):
self.symbol = self.symbol.strip().upper()
if self.price <= 0:
raise ValueError("price must be positive")
def notional(self):
return self.price * abs(self.quantity)
@classmethod
def from_line(cls, line):
symbol, price, quantity = line.split(",")
return cls(symbol, float(price), int(quantity))
t = Trade.from_line(" aapl,184.25,-200")
print(t) # Trade(symbol='AAPL', price=184.25, quantity=-200)
print(t.notional()) # 36850.0frozen、order、そして通常のクラスが向くとき
@dataclass(frozen=True) はインスタンスを読み取り専用にし(フィールドへの代入は FrozenInstanceError)、ハッシュも与えるので、辞書のキーや集合の要素にできます。契約仕様のように変わってはいけない値に最適です。@dataclass(order=True) は、フィールドを順に比較する <、<=、>、>= を追加します。データのレコードにはデータクラスを使い、振る舞いや隠れた状態が多いオブジェクトには、丁寧にメソッドを書いた通常のクラスのほうが分かりやすくなります。
from dataclasses import dataclass
@dataclass(frozen=True, order=True)
class Contract:
symbol: str
expiry: str # ISO dates sort correctly as text
a = Contract("ESZ5", "2025-12-19")
b = Contract("ESZ5", "2026-03-20")
print(a < b) # True
print({a: "front", b: "next"}[a]) # front (frozen => hashable)
try:
a.symbol = "NQZ5"
except Exception as error:
print(type(error).__name__) # FrozenInstanceErrorprice: float のような注釈では、Trade("AAPL", "abc", 1) の作成は防げません。不正な値が後の計算を黙って壊すおそれがあるなら、__post_init__ で検査して ValueError を送出し、壊れたレコードが存在できないようにします。検査を回避できないよう、from_line を含め、オブジェクトは常に通常のコンストラクター経由で作りましょう。@dataclassは注釈付きフィールドから__init__、__repr__、__eq__を作る- デフォルト付きは後ろに置き、リストや辞書には
field(default_factory=list)を使う __post_init__で検証し、@classmethodで別の形から作り、frozen=Trueで読み取り専用にする
次はコードで微分です。量の変化の速さを推定します。デルタやガンマといったオプション感応度の考え方です。
ラボ:採点される内容
__post_init__で約定を整形して検証する25点notionalで約定金額を計算する25点from_lineでテキストから約定を作る25点net_quantityで1つのシンボルの約定を差し引き合計する25点