7
@dataclass
class Stock:
    symbol: str
    price: float = get_price(symbol)

Can a dataclass attribute access to the other one? In the above example, one can create a Stock by providing a symbol and the price. If price is not provided, it defaults to a price which we get from some function get_price. Is there a way to reference symbol?

This example generates error NameError: name 'symbol' is not defined.

Mukesh C
  • 93
  • 7
  • 2
    Complicated initialization behaviour like that belongs in a proper class instead of a dataclass. – rdas Sep 04 '22 at 13:28
  • I agree, but we may differ if this case should be considered complex. There is no business logic except getting a price if not provided. Function as default value works fine in `dataclasses` except the problem I discussed (not able to refer to another parameter). I think the answer from @S.B. below can help use `dataclass` in such simple cases. – Mukesh C Sep 04 '22 at 23:46

2 Answers2

11

You can use __post_init__ here. Because it's going to be called after __init__, you have your attributes already populated so do whatever you want to do there:

from typing import Optional
from dataclasses import dataclass


def get_price(name):
    # logic to get price by looking at `name`.
    return 1000.0


@dataclass
class Stock:
    symbol: str
    price: Optional[float] = None

    def __post_init__(self):
        if self.price is None:
            self.price = get_price(self.symbol)


obj1 = Stock("boo", 2000.0)
obj2 = Stock("boo")
print(obj1.price)  # 2000.0
print(obj2.price)  # 1000.0

So if user didn't pass price while instantiating, price is None. So you can check it in __post_init__ and ask it from get_price.


There is also another shape of the above answer which basically adds nothing more to the existing one. I just added for the records since someone might attempt to do this as well and wonder how is it different with the previous one:

@dataclass
class Stock:
    symbol: str
    price: InitVar[Optional[float]] = None

    def __post_init__(self, price):
        self.price = get_price(self.symbol) if price is None else price

You mark the price as InitVar and you can get it with a parameter named price in the __post_init__ method.

S.B
  • 13,077
  • 10
  • 22
  • 49
  • 1
    This is brilliant - the cleanest solution. – Mukesh C Sep 04 '22 at 23:28
  • Based on the post-init docs referenced, better yet (if you explicitly don't want price to be part of the constructor), then `price: float = dataclasses.field(init=False)` – tplusk Mar 10 '23 at 16:28
4

If the price is always derivable from the symbol, I would go with a @property. Keeps the code lean and it looks like an attribute from the outside:

def get_price(symbol):
    return 123

@dataclass
class Stock:
    symbol: str

@property
def price(self):
    return get_price(symbol)

stock = Stock("NVDA")
print(stock.price) # 123

If you want to have a settable attribute that also has a default value that is derived from the other fields, you may just want to implement your own __init__ or __post_init__ as suggested.

If this is something you encounter a lot and need more sophisticated ways to handle it, I would recommend looking into pydantic and validators.

from typing import Optional

from pydantic import validator
from pydantic.dataclasses import dataclass

def get_price(symbol):
    return 123.0

@dataclass
class Stock:
    symbol: str
    price: Optional[float] = None

    @validator('price')
    def validate_price(cls, v, values, **kwargs):
        return v or get_price(values["symbol"])


print(Stock("NVDA"))
#> Stock(symbol='NVDA', price=123.0)
print(Stock("NVDA", 456))
#> Stock(symbol='NVDA', price=456.0)
mmdanziger
  • 4,466
  • 2
  • 31
  • 47
  • I thought of property before asking, but that changes the workflow if someone really has a value to provide (default only in case no value provided). `pydantic` looks nice but looks more involved in this use case. I'll go through the docs to understand the use cases. – Mukesh C Sep 04 '22 at 23:35