Lección 0.4.1 · 45 min
Programación orientada a objetos: clases, herencia, composición y dataclasses
Un sistema de trading maneja objetos con estado y reglas: órdenes que no deben cambiar, posiciones que sí cambian y carteras que agrupan posiciones. Esta lección los modela con clases y dataclasses, y explica cuándo heredar y cuándo componer.
- Módulo
- 0.4 · Python intermedio
- Duración
- 45 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
Por qué clases
Hasta ahora los datos de una orden cabían en un diccionario: {"simbolo": "SINT", "lado": "compra", "cantidad": 100}.
Funciona, pero no protege de nada. Un error de tecleo ("cantdad") crea una clave nueva en silencio, nada impide una
cantidad negativa y el cálculo del importe vive en una función suelta que hay que acordarse de llamar.
Una clase junta en un mismo sitio los datos (atributos) y lo que se puede hacer con ellos (métodos). Cada objeto creado a partir de la clase es una instancia. En un sistema de trading hay tres candidatas evidentes:
| Objeto | Qué guarda | ¿Cambia? | Qué hace |
|---|---|---|---|
| Orden | símbolo, lado, cantidad, precio | No: una vez enviada, se cancela o se sustituye | Calcula su importe, se valida |
| Posición | símbolo, cantidad, precio medio, P&L realizado | Sí: con cada ejecución | Aplica ejecuciones, calcula su valor |
| Cartera | efectivo y posiciones | Sí | Ejecuta órdenes, calcula valor y P&L |
La columna «¿Cambia?» decide buena parte del diseño. Las órdenes serán inmutables; las posiciones y la cartera, no.
Una clase escrita a mano
La forma básica de una clase: __init__ recibe los datos y los guarda en self, la propia instancia. Los métodos
son funciones que reciben self como primer argumento. Los que empiezan y terminan con doble guion bajo son
métodos especiales: Python los llama por su cuenta. __repr__ decide cómo se muestra el objeto y __eq__, cuándo
dos objetos son iguales.
class OrdenManual:
def __init__(self, simbolo, lado, cantidad, precio):
self.simbolo = simbolo
self.lado = lado
self.cantidad = cantidad
self.precio = precio
def importe(self):
return self.cantidad * self.precio
def __repr__(self):
return (f"OrdenManual({self.simbolo!r}, {self.lado!r}, "
f"{self.cantidad}, {self.precio})")
def __eq__(self, otra):
if not isinstance(otra, OrdenManual):
return NotImplemented
return ((self.simbolo, self.lado, self.cantidad, self.precio)
== (otra.simbolo, otra.lado, otra.cantidad,
otra.precio))
a = OrdenManual("SINT", "compra", 100, 142.22)
b = OrdenManual("SINT", "compra", 100, 142.22)
print(a)
print(f"Importe: {a.importe():.2f}")
print(a == b, a is b)OrdenManual('SINT', 'compra', 100, 142.22)
Importe: 14222.00
True FalseSin __repr__, print(a) mostraría algo como <OrdenManual object at 0x…>, inútil en un registro de operaciones.
Sin __eq__, a == b compararía la identidad (si son el mismo objeto en memoria) y daría False para dos órdenes
idénticas. Con los dos métodos, a == b es True aunque a is b siga siendo False: son dos objetos con el mismo
contenido.
El importe se imprime con dos decimales por precaución. 100 * 142.22 da 14222.0 justo, pero 3 * 142.22 da
426.65999999999997: es el problema de coma flotante de la lección 0.3.1, y la razón de que en contabilidad de
precios se use decimal.
Hay mucho código repetido: cada campo aparece cuatro veces en __init__, otra en __repr__ y dos en __eq__. Si
mañana se añade un campo y se olvida en __eq__, dos órdenes distintas serán «iguales».
Dataclasses: el mismo resultado sin repetir
El decorador @dataclass (módulo dataclasses, en la biblioteca estándar) lee las anotaciones de la clase y escribe
__init__, __repr__ y __eq__ por ti. Las anotaciones (simbolo: str) se ven a fondo en la lección 0.4.2; aquí
basta con saber que cada línea nombre: tipo declara un campo.
Dos opciones encajan con una orden:
frozen=True: la instancia no se puede modificar después de crearla. Una orden enviada no cambia; si se quiere otra cantidad, se crea otra orden.slots=True: los atributos se guardan en huecos fijos en lugar de en un diccionario. Ocupa menos memoria y hace imposible crear atributos por error.
La validación va en __post_init__, que la dataclass llama al final de su __init__. El importe es una
propiedad (@property): se calcula al leerla, sin paréntesis, y no se guarda.
from dataclasses import dataclass, field, replace
LADOS = ("compra", "venta")
@dataclass(frozen=True, slots=True)
class Orden:
simbolo: str
lado: str
cantidad: int
precio: float
def __post_init__(self):
if self.lado not in LADOS:
raise ValueError(f"lado no válido: {self.lado!r}")
if self.cantidad <= 0:
raise ValueError(f"cantidad no positiva: {self.cantidad}")
@property
def importe(self) -> float:
return self.cantidad * self.precio
orden = Orden("SINT", "compra", 100, 142.22)
print(orden)
print(orden == Orden("SINT", "compra", 100, 142.22))
print(f"{orden.importe:.2f}")Orden(simbolo='SINT', lado='compra', cantidad=100, precio=142.22)
True
14222.00Las garantías se comprueban en cuanto se intenta saltárselas. Modificar un campo de una orden congelada lanza
FrozenInstanceError:
orden.cantidad = 200dataclasses.FrozenInstanceError: cannot assign to field 'cantidad'Y una orden mal construida no llega a existir:
Orden("SINT", "comprar", 100, 142.22)ValueError: lado no válido: 'comprar'Para cambiar algo se crea una orden nueva con replace, que copia todos los campos menos los indicados y vuelve a
pasar por __post_init__. Como las órdenes congeladas tienen __hash__, también se pueden meter en un conjunto, lo
que sirve, por ejemplo, para descartar órdenes duplicadas que una señal haya generado dos veces:
reducida = replace(orden, cantidad=50)
print(reducida)
print(orden.cantidad)
pendientes = {orden, Orden("SINT", "compra", 100, 142.22), reducida}
print(len(pendientes))Orden(simbolo='SINT', lado='compra', cantidad=50, precio=142.22)
100
2La original sigue con 100 títulos y el conjunto tiene dos órdenes, no tres: las dos primeras son iguales.
Orden y prioridad: precio-tiempo
Un libro de órdenes ejecuta primero la orden con mejor precio y, a igual precio, la que llegó antes. Es la prioridad
precio-tiempo. Con order=True, una dataclass compara sus campos como una tupla, en el orden en que se declaran,
así que basta con declarar primero el precio y después la llegada. Los campos que no deben contar en la comparación
llevan field(compare=False):
@dataclass(frozen=True, order=True)
class OrdenLibro:
precio: float
llegada: int
id: str = field(compare=False)
cantidad: int = field(compare=False)
ventas = [
OrdenLibro(142.30, 3, "v3", 200),
OrdenLibro(142.25, 5, "v5", 100),
OrdenLibro(142.30, 1, "v1", 50),
OrdenLibro(142.25, 2, "v2", 300),
]
for o in sorted(ventas):
print(o.id, o.precio, o.llegada, o.cantidad)v2 142.25 2 300
v5 142.25 5 100
v1 142.3 1 50
v3 142.3 3 200Para las ventas, el mejor precio es el más bajo, que es justo el orden natural. Para las compras, el mejor precio es el más alto y el truco no sirve tal cual: el ejercicio 2 lo resuelve con una clave de ordenación.
Posición y cartera: composición
Una posición sí cambia: cada ejecución modifica su cantidad y su precio medio, y al reducirla se realiza un
beneficio o una pérdida. Es una dataclass normal, sin frozen. El método aplicar recibe una cantidad con signo
(positiva para comprar, negativa para vender):
@dataclass
class Posicion:
simbolo: str
cantidad: int = 0
precio_medio: float = 0.0
pnl_realizado: float = 0.0
def aplicar(self, cantidad: int, precio: float) -> None:
"""Suma una ejecución: cantidad > 0 compra; < 0 vende."""
if self.cantidad == 0 or self.cantidad * cantidad > 0:
total = self.cantidad + cantidad # abre o amplía
self.precio_medio = (self.cantidad * self.precio_medio
+ cantidad * precio) / total
self.cantidad = total
return
signo = 1 if self.cantidad > 0 else -1 # reduce
cerrada = min(abs(cantidad), abs(self.cantidad))
self.pnl_realizado += signo * cerrada * (
precio - self.precio_medio)
self.cantidad += cantidad
if self.cantidad == 0:
self.precio_medio = 0.0
elif self.cantidad * signo < 0: # ha dado la vuelta
self.precio_medio = precio
def valor(self, precio: float) -> float:
return self.cantidad * precio
def pnl_no_realizado(self, precio: float) -> float:
return self.cantidad * (precio - self.precio_medio)
p = Posicion("RVAR")
p.aplicar(100, 150.0)
p.aplicar(50, 160.0)
p.aplicar(-80, 166.0)
print(p)Posicion(simbolo='RVAR', cantidad=70, precio_medio=153.33333333333334, pnl_realizado=1013.3333333333326)Las dos compras dejan 150 títulos a un precio medio de 153,33; la venta de 80 a 166 realiza
80 × (166 − 153,33) = 1.013,33 de beneficio, y quedan 70 títulos al mismo precio medio. El repr de la dataclass
muestra los floats tal cual (153.33333333333334, 1013.3333333333326): para leerlos se redondean al imprimir, como
en la celda siguiente.
La cartera no es una posición: tiene posiciones, y efectivo. Eso es composición: un objeto que contiene
otros y les delega el trabajo. La cartera solo sabe convertir una Orden en una ejecución con signo, cobrar la
comisión y sumar.
Un detalle de las dataclasses: un valor por defecto mutable, como un diccionario vacío, sería el mismo objeto para todas las carteras. La dataclass no lo permite:
@dataclass
class CarteraMal:
efectivo: float
posiciones: dict = {}ValueError: mutable default <class 'dict'> for field posiciones is not allowed: use default_factoryLa forma correcta es field(default_factory=dict), que crea un diccionario nuevo para cada instancia:
@dataclass
class Cartera:
efectivo: float
comision: float = 1.0 # euros por orden
posiciones: dict[str, Posicion] = field(default_factory=dict)
def ejecutar(self, orden: Orden) -> None:
signo = 1 if orden.lado == "compra" else -1
if orden.simbolo not in self.posiciones:
self.posiciones[orden.simbolo] = Posicion(orden.simbolo)
self.posiciones[orden.simbolo].aplicar(
signo * orden.cantidad, orden.precio)
self.efectivo -= signo * orden.importe + self.comision
def valor(self, precios: dict[str, float]) -> float:
return self.efectivo + sum(
p.valor(precios[s]) for s, p in self.posiciones.items())
def pnl(self, precios: dict[str, float]) -> tuple[float, float]:
realizado = sum(p.pnl_realizado
for p in self.posiciones.values())
no_realizado = sum(p.pnl_no_realizado(precios[s])
for s, p in self.posiciones.items())
return realizado, no_realizado
cartera = Cartera(efectivo=50_000)
for o in [Orden("RVAR", "compra", 100, 150.0),
Orden("ORO", "compra", 50, 165.0),
Orden("RVAR", "compra", 50, 160.0),
Orden("RVAR", "venta", 80, 166.0)]:
cartera.ejecutar(o)
# cierres del 2025-12-31 en cartera_sint.csv
cierres = {"RVAR": 164.35, "RFIJ": 102.29, "ORO": 171.44,
"TECH": 77.82}
realizado, no_realizado = cartera.pnl(cierres)
print(f"Efectivo: {cartera.efectivo:>10,.2f}")
print(f"Valor total: {cartera.valor(cierres):>10,.2f}")
print(f"Realizado: {realizado:>10,.2f}")
print(f"No realizado: {no_realizado:>10,.2f}")
print(f"Comisiones: {4 * cartera.comision:>10,.2f}")Efectivo: 32,026.00
Valor total: 52,102.50
Realizado: 1,013.33
No realizado: 1,093.17
Comisiones: 4.00Las cifras cuadran: el valor total menos los 50.000 iniciales es igual al P&L realizado más el no realizado menos las cuatro comisiones. Esa identidad es una buena candidata a test automático (lección 0.4.5): si algún día deja de cumplirse, hay un error en la contabilidad.
Herencia: variantes de una orden
La herencia crea una clase a partir de otra: la subclase recibe los atributos y métodos de la clase base y puede añadir o sustituir los suyos. Encaja cuando un objeto es un caso particular de otro. Una orden de mercado, una limitada y una de stop son órdenes; lo que cambia entre ellas es cuándo se ejecutan.
La clase base declara el método se_ejecuta como abstracto (abc.ABC y @abstractmethod): no se puede crear una
orden genérica, solo una de sus variantes. Cada subclase añade sus campos y su regla:
from abc import ABC, abstractmethod
@dataclass(frozen=True)
class OrdenBase(ABC):
simbolo: str
lado: str
cantidad: int
@abstractmethod
def se_ejecuta(self, precio: float) -> bool: ...
@dataclass(frozen=True)
class OrdenMercado(OrdenBase):
def se_ejecuta(self, precio: float) -> bool:
return True
@dataclass(frozen=True)
class OrdenLimitada(OrdenBase):
limite: float
def se_ejecuta(self, precio: float) -> bool:
if self.lado == "compra":
return precio <= self.limite
return precio >= self.limite
@dataclass(frozen=True)
class OrdenStop(OrdenBase):
stop: float
def se_ejecuta(self, precio: float) -> bool:
if self.lado == "venta":
return precio <= self.stop
return precio >= self.stop
ordenes = [OrdenMercado("SINT", "compra", 10),
OrdenLimitada("SINT", "compra", 10, limite=140.0),
OrdenStop("SINT", "venta", 10, stop=138.0)]
for precio in (142.0, 139.5, 137.8):
activas = [type(o).__name__ for o in ordenes
if o.se_ejecuta(precio)]
print(precio, activas)142.0 ['OrdenMercado']
139.5 ['OrdenMercado', 'OrdenLimitada']
137.8 ['OrdenMercado', 'OrdenLimitada', 'OrdenStop']El bucle no pregunta de qué tipo es cada orden: llama a se_ejecuta y cada objeto responde con su regla. Eso es
polimorfismo, y es lo que hace útil la herencia. Añadir una orden de stop limitada no obliga a tocar el bucle,
solo a escribir una subclase más. Intentar crear la clase base falla, como se quería:
OrdenBase("SINT", "compra", 10)TypeError: Can't instantiate abstract class OrdenBase without an implementation for abstract method 'se_ejecuta'Una trampa de las dataclasses con herencia: los campos de la subclase van después de los de la base. Si la base
tuviera un campo con valor por defecto, la subclase no podría añadir uno sin él (TypeError: non-default argument follows default argument). La salida es declarar los campos como solo-palabra-clave con @dataclass(kw_only=True),
o no poner valores por defecto en la base.
Herencia o composición
Las dos relaciones se dibujan distinto. Las flechas «es una» son herencia; las flechas «tiene» y «ejecuta», composición:
Fuente Mermaid
flowchart LR M[OrdenMercado] -->|es una| B[OrdenBase] L[OrdenLimitada] -->|es una| B S[OrdenStop] -->|es una| B C[Cartera] -->|tiene| P[Posicion] C -->|ejecuta| O[Orden]
La regla práctica: hereda solo si la frase «X es un Y» es cierta y lo seguirá siendo; en cualquier otro caso, compón. Los errores típicos en código de trading van casi siempre en la misma dirección, heredar de más:
class Estrategia(Cartera): una estrategia no es una cartera. Si mañana dos estrategias comparten cartera, o una estrategia reparte capital entre dos, la herencia no lo permite.class MiEstrategia(EstrategiaMediasMoviles, GestorRiesgo, ConectorBroker): herencia múltiple para «reunir funciones». Cada clase base puede pisar métodos de las otras, y probar la estrategia exige un bróker.- Jerarquías de cinco niveles (
Estrategia→EstrategiaTendencia→EstrategiaMedias→ …) en las que un cambio arriba rompe algo abajo.
Con composición, la estrategia recibe sus piezas al crearla. Cambiar el gestor de riesgo, o usar uno falso en un test, no exige tocar la estrategia:
@dataclass
class LimiteRiesgo:
max_importe: float
def permite(self, orden: Orden) -> bool:
return orden.importe <= self.max_importe
@dataclass
class Ejecutor:
cartera: Cartera
riesgo: LimiteRiesgo
rechazadas: list[Orden] = field(default_factory=list)
def enviar(self, orden: Orden) -> bool:
if not self.riesgo.permite(orden):
self.rechazadas.append(orden)
return False
self.cartera.ejecutar(orden)
return True
ejecutor = Ejecutor(Cartera(efectivo=20_000),
LimiteRiesgo(max_importe=10_000))
print(ejecutor.enviar(Orden("TECH", "compra", 100, 77.82)))
print(ejecutor.enviar(Orden("ORO", "compra", 100, 171.44)))
print(ejecutor.cartera.posiciones)
print([o.simbolo for o in ejecutor.rechazadas])True
False
{'TECH': Posicion(simbolo='TECH', cantidad=100, precio_medio=77.82, pnl_realizado=0.0)}
['ORO']La compra de TECH (7.782 €) pasa el límite; la de ORO (17.144 €) no, y queda registrada como rechazada. Para un
límite distinto, por ejemplo por exposición total de la cartera, basta con otra clase que tenga un método permite
con la misma firma. Cómo declarar formalmente esa «firma común» (un Protocol) se ve en la lección 0.4.2.
Ejercicios
- Añade a
Ordenun campo opcionaletiqueta: str = ""(por ejemplo, el nombre de la estrategia que la generó) que no cuente al comparar órdenes. Comprueba que dos órdenes iguales salvo la etiqueta son iguales.
Solución
@dataclass(frozen=True, slots=True)
class OrdenEtiquetada:
simbolo: str
lado: str
cantidad: int
precio: float
etiqueta: str = field(default="", compare=False)
x = OrdenEtiquetada("SINT", "compra", 100, 142.22, "cruce")
y = OrdenEtiquetada("SINT", "compra", 100, 142.22, "ruptura")
print(x == y, hash(x) == hash(y))True TrueCon compare=False el campo queda fuera de __eq__ y de __hash__: las dos órdenes son iguales y caen en el mismo
hueco de un conjunto. El campo con valor por defecto va al final, después de los que no lo tienen.
- Ordena estas órdenes de compra por prioridad precio-tiempo (mejor precio = el más alto; a igual precio, la que
llegó antes):
OrdenLibro(142.20, 4, "c4", 100),OrdenLibro(142.25, 6, "c6", 50),OrdenLibro(142.20, 1, "c1", 200).
Solución
compras = [OrdenLibro(142.20, 4, "c4", 100),
OrdenLibro(142.25, 6, "c6", 50),
OrdenLibro(142.20, 1, "c1", 200)]
for o in sorted(compras, key=lambda o: (-o.precio, o.llegada)):
print(o.id, o.precio, o.llegada)c6 142.25 6
c1 142.2 1
c4 142.2 4La clave (-precio, llegada) invierte el precio y mantiene el tiempo: primero 142,25 y, a 142,20, la orden que llegó
en el instante 1 antes que la del instante 4.
- Con la
Carterade la lección, abre un corto: vende 40 TECH a 80 y recompra 40 a 77,82. ¿Cuál es el P&L realizado? ¿Qué cantidad y precio medio quedan en la posición?
Solución
corta = Cartera(efectivo=10_000)
corta.ejecutar(Orden("TECH", "venta", 40, 80.0))
corta.ejecutar(Orden("TECH", "compra", 40, 77.82))
print(corta.posiciones["TECH"])
print(f"Efectivo: {corta.efectivo:,.2f}")Posicion(simbolo='TECH', cantidad=0, precio_medio=0.0, pnl_realizado=87.20000000000027)
Efectivo: 10,085.20El corto gana 40 × (80 − 77,82) = 87,20 y la posición vuelve a cero con precio medio cero. El repr muestra
87.20000000000027, no 87.2: 80 - 77.82 da 2.180000000000007 en coma flotante (lección 0.3.1). Es un error de
representación, no de contabilidad, pero obliga a dos cosas: redondear al mostrar y comparar con tolerancia en los
tests (pytest.approx, lección 0.4.5), nunca con ==. El efectivo sube esos 87,20 menos las dos comisiones de 1 €.
Posicion.aplicar trata igual largos y cortos porque trabaja con cantidades con signo.
Respuesta plegada en cada pregunta
Autoevaluación
Una orden ya enviada al bróker no debe cambiar de cantidad por accidente. ¿Qué opción lo garantiza en Python?
- AUn diccionario con la clave
cantidad - BUna dataclass con
frozen=True: asignar un campo lanzaFrozenInstanceError - CUna clase normal con un comentario que diga «no modificar»
- DUna dataclass con
order=True
Ver respuesta
Respuesta B. Una dataclass con
frozen=True: asignar un campo lanzaFrozenInstanceErrorfrozen=Truehace que asignar un campo después de crear el objeto lance una excepción. Para «cambiar» una orden se crea otra condataclasses.replace, y la original queda intacta.order=Truesolo añade comparaciones.- AUn diccionario con la clave
Una dataclass tiene el campo
posiciones: dict = {}. ¿Qué pasa al definir la clase?- ANada: cada cartera tendrá su propio diccionario vacío
- BTodas las carteras comparten el mismo diccionario, sin aviso
- CLanza
ValueError: los valores por defecto mutables se declaran confield(default_factory=dict) - DLanza
TypeErroral crear la primera cartera
Ver respuesta
Respuesta C. Lanza
ValueError: los valores por defecto mutables se declaran confield(default_factory=dict)Un
{}por defecto sería el mismo objeto para todas las instancias. La dataclass lo detecta al definir la clase y lanzaValueError;default_factorycrea un diccionario nuevo por instancia.Con
@dataclass(order=True)y los camposprecioyllegada(en ese orden), ¿cómo ordenasorted()una lista de órdenes de venta?- APor precio y, a igual precio, por llegada: la prioridad precio-tiempo de un libro de órdenes
- BSolo por precio
- CPor el orden en que se crearon los objetos
- DAl azar, porque los objetos no se pueden comparar
Ver respuesta
Respuesta A. Por precio y, a igual precio, por llegada: la prioridad precio-tiempo de un libro de órdenes
order=Truecompara los campos como una tupla, en el orden en que se declaran. Los campos confield(compare=False)quedan fuera de la comparación.Una estrategia necesita una cartera y un gestor de riesgo. ¿Qué diseño sigue la regla de la lección?
- A
class Estrategia(Cartera, GestorRiesgo) - B
class Estrategia(Cartera)y el riesgo como métodos de la estrategia - CUna estrategia que tiene como atributos una cartera y un gestor de riesgo (composición)
- DVariables globales compartidas por todos los módulos
Ver respuesta
Respuesta C. Una estrategia que tiene como atributos una cartera y un gestor de riesgo (composición)
Una estrategia no es una cartera: tiene una. Con composición se puede cambiar el gestor de riesgo sin tocar la estrategia, y cada pieza se prueba por separado.
- A
Ideas clave
- Una clase junta datos y comportamiento.
__repr__y__eq__deciden cómo se imprime y cuándo dos objetos son iguales. @dataclassescribe__init__,__repr__y__eq__por ti;frozen=Trueimpide modificar un objeto y lo hace utilizable en conjuntos y como clave de diccionario.order=Truecompara los campos como una tupla: la prioridad precio-tiempo de un libro de órdenes sale de declarar los campos en ese orden.- Los valores por defecto mutables van con
field(default_factory=…); la dataclass impide el error clásico de compartir una lista o un diccionario entre instancias. - Hereda cuando el objeto es una variante de otro (una orden limitada es una orden); compón cuando lo usa (una estrategia tiene una cartera).