为保存数据的类减少样板代码
很多类的主要作用就是保存几个字段:一笔成交、一条报价、一份配置。为每个类都写 __init__、__repr__ 和 __eq__ 很重复。dataclasses 模块的 @dataclass 装饰器会根据一组带注解的字段替你生成它们。你只需声明一次 symbol: str、price: float、quantity: int,就得到构造函数 Trade("AAPL", 184.25, 200)、可读的 repr(如 Trade(symbol='AAPL', price=184.25, quantity=200))以及逐字段比较的相等性。注解用于说明意图,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 以 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.0冻结、排序,以及何时普通类更合适
@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使值只读。
下一节:用代码求导数,估计某个量变化得有多快,这正是 Delta、Gamma 等期权敏感度背后的思想。
实验:评分内容
- 用
__post_init__清理并校验成交25 分 - 用
notional计算成交金额25 分 - 用
from_line从文本构建成交25 分 - 用
net_quantity汇总某个代码的净成交量25 分