Moins de code répétitif pour les classes qui portent des données
Beaucoup de classes existent surtout pour contenir quelques champs : une transaction, une cotation, une configuration. Écrire __init__, __repr__ et __eq__ pour chacune est répétitif. Le décorateur @dataclass du module dataclasses les écrit pour vous à partir d'une liste de champs annotés. Vous déclarez une fois symbol: str, price: float, quantity: int et vous obtenez un constructeur Trade("AAPL", 184.25, 200), un repr lisible comme Trade(symbol='AAPL', price=184.25, quantity=200) et une égalité champ par champ. Les annotations documentent l'intention ; Python ne les impose pas.
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.0Valeurs par défaut, field() et valeurs modifiables
Un champ peut avoir une valeur par défaut (currency: str = "USD"), et les champs avec défaut doivent venir après ceux qui n'en ont pas. Pour un défaut modifiable comme une liste, un simple = [] serait partagé par tous les objets (et Python le refuse) ; écrivez trades: list = field(default_factory=list) pour que chaque objet reçoive sa propre liste neuve. field(...) permet aussi de masquer un champ du repr (repr=False) ou des comparaisons (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=[])Vos méthodes, __post_init__ et constructeurs alternatifs
Une dataclass reste une classe ordinaire : on peut y ajouter des méthodes. __post_init__(self) s'exécute juste après le __init__ généré, c'est l'endroit pour valider ou nettoyer les valeurs (self.symbol = self.symbol.upper(), raise ValueError(...) pour un mauvais prix). Une @classmethod reçoit la classe sous le nom cls et c'est la façon habituelle de construire un objet depuis une autre forme, comme du texte : Trade.from_line("AAPL,184.25,200") renvoie cls(symbol, float(price), int(quantity)), si bien que la validation s'applique toujours et que les sous-classes fonctionnent aussi.
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.0Figé, ordonné, et quand les classes ordinaires gagnent
@dataclass(frozen=True) rend les instances en lecture seule (assigner à un champ lève FrozenInstanceError) et leur donne un hachage, ce qui permet de les utiliser comme clés de dictionnaire ou membres d'ensemble : idéal pour des valeurs qui ne doivent pas changer, comme la spécification d'un contrat. @dataclass(order=True) ajoute <, <=, >, >= en comparant les champs dans l'ordre. Utilisez une dataclass pour des enregistrements de données ; pour des objets riches en comportement et en état caché, une classe écrite à la main avec des méthodes soignées est plus claire.
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 n'empêchent pas la création de Trade("AAPL", "abc", 1). Si de mauvaises valeurs corrompaient silencieusement des calculs ultérieurs, vérifiez-les dans __post_init__ et levez ValueError, afin qu'un enregistrement cassé ne puisse pas exister. Créez toujours les objets via le constructeur normal, y compris depuis from_line, pour que la vérification ne puisse pas être contournée.@dataclasstransforme des champs annotés en__init__,__repr__et__eq__.- Les défauts viennent en dernier ; utilisez
field(default_factory=list)pour les listes et dictionnaires. __post_init__valide ;@classmethodconstruit depuis d'autres formes ;frozen=Truerend les valeurs en lecture seule.
À suivre : les dérivées en code, estimer la vitesse de variation d'une grandeur, l'idée derrière les sensibilités d'options comme delta et gamma.
Lab : ce qui est noté
- Nettoyer et valider une transaction avec
__post_init__25 pts - Calculer la valeur échangée avec
notional25 pts - Construire une transaction depuis du texte avec
from_line25 pts - Compenser les transactions d'un symbole avec
net_quantity25 pts