使用 attrs 创建具有互斥参数的 Python class
Creating Python class with mutually exclusive arguments using attrs
我有一个 class,它有两个互斥的参数(prices
和 returns
)。也就是说,不能为了实例化一个对象而同时提供它们。
但是,class 需要两者进行内部计算。所以我想根据用户提供的 pd.Series
计算缺失的
我创建了两个可供选择的 class 构造函数(from_prices
和 from_returns
)。使用这些构造函数,class 将被正确实例化。
这是代码。它使用 attrs
库 (www.attrs.org).
import pandas as pd
import attr
@attr.s
class MutuallyExclusive:
prices: pd.Series = attr.ib()
returns: pd.Series = attr.ib()
trading_days_per_year: int = attr.ib(default=252)
@classmethod
def from_prices(cls, price_series: pd.Series, trading_days: int = 252):
return cls(
price_series,
price_series.pct_change(),
trading_days,
)
@classmethod
def from_returns(cls, return_series: pd.Series):
return cls(
pd.Series(data=100 + 100 * (returns.add(1).cumprod() - 1)),
return_series,
)
if __name__ == "__main__":
prices = pd.Series(data=[100, 101, 98, 104, 102, 108])
returns = pd.Series(data=[0.01, 0.03, -0.02, 0.01, -0.03, 0.04])
obj_returns = MutuallyExclusive.from_returns(returns)
obj_prices = MutuallyExclusive.from_prices(prices, trading_days=100)
但是,尽管这两个系列彼此不兼容,用户仍然可以调用 obj = MutuallyExclusive(prices, returns)
。捕捉这种情况并抛出错误的最佳方法是什么?
编辑:
是否可以一起“禁用”常规构造函数?如果可以仅通过替代构造函数实例化对象,这将解决问题,不是吗?
您在 __attrs_post_init__
中检查:https://www.attrs.org/en/stable/init.html#post-init
attrs
库是正确的工具吗?
为什么不使用常规 python class 并自己定义 __init__()
?
import pandas as pd
class MutuallyExclusive:
def __init__(self, prices: pd.Series = None, returns: pd.Series = None):
if prices is not None and returns is not None:
raise ValueError("prices and returns are mutually exclusive")
self.prices = prices if prices is not None else pd.Series(data=100 * (1 + returns))
self.returns = returns if returns is not None else prices.pct_change()
if __name__ == "__main__":
prices = pd.Series(data=[100, 101, 98, 104, 102, 108])
returns = pd.Series(data=[0.01, 0.03, -0.02, 0.01, -0.03, 0.04])
obj_returns = MutuallyExclusive(returns=returns)
obj_prices = MutuallyExclusive(prices=prices)
编辑:您更新了您的示例,所以我的回答缺少 trading_days_per_year
但概念是相同的。
如果你想使用 attrs
库,其他人已经指出你可以将你的逻辑放在 __attrs_post_init__
函数中,请参见下面的示例删除对 class 的需要方法
请注意,您需要将价格和 returns 都默认为 None
def __attrs_post_init__(self):
if self.prices is not None and self.returns is not None:
raise ValueError("prices and returns are mutually exclusive")
if self.returns is None:
self.returns = self.price_series.pct_change()
if self.prices is None:
self.prices = pd.Series(data=100 + 100 * (self.returns.add(1).cumprod() - 1))
我不知道是否有更惯用的模式,但你可以用布尔锁保护构造函数,检查 __attrs_post_init__
中的锁以防止直接调用构造函数:
import pandas as pd
import attr
@attr.s
class MutuallyExclusive:
prices: pd.Series = attr.ib()
returns: pd.Series = attr.ib()
def __attrs_post_init__(self):
if not MutuallyExclusive.constructor_unlocked:
raise TypeError('Please use the `from_prices` or `from_returns` constructor methods')
@classmethod
def from_prices(cls, price_series: pd.Series):
cls.constructor_unlocked = True
value = cls(price_series, price_series.pct_change())
cls.constructor_unlocked = False
return value
@classmethod
def from_returns(cls, return_series: pd.Series):
cls.constructor_unlocked = True
value = cls(pd.Series(data=100 * (1 + return_series)), return_series)
cls.constructor_unlocked = False
return value
MutuallyExclusive.constructor_unlocked = False
if __name__ == "__main__":
prices = pd.Series(data=[100, 101, 98, 104, 102, 108])
returns = pd.Series(data=[0.01, 0.03, -0.02, 0.01, -0.03, 0.04])
obj_returns = MutuallyExclusive.from_returns(returns)
obj_prices = MutuallyExclusive.from_prices(prices)
bad = MutuallyExclusive(obj_prices.prices, obj_prices.returns)
或者,如果您愿意,threading.Lock
:
import pandas as pd
import attr
from threading import Lock
mutually_exclusive_constructor_lock = Lock()
@attr.s
class MutuallyExclusive:
prices: pd.Series = attr.ib()
returns: pd.Series = attr.ib()
def __attrs_post_init__(self):
if not mutually_exclusive_constructor_lock.locked():
raise TypeError('Please use the `from_prices` or `from_returns` constructor methods')
@classmethod
def from_prices(cls, price_series: pd.Series):
with mutually_exclusive_constructor_lock:
return cls(price_series, price_series.pct_change())
@classmethod
def from_returns(cls, return_series: pd.Series):
with mutually_exclusive_constructor_lock:
return cls(pd.Series(data=100 * (1 + return_series)), return_series)
if __name__ == "__main__":
prices = pd.Series(data=[100, 101, 98, 104, 102, 108])
returns = pd.Series(data=[0.01, 0.03, -0.02, 0.01, -0.03, 0.04])
obj_returns = MutuallyExclusive.from_returns(returns)
obj_prices = MutuallyExclusive.from_prices(prices)
bad = MutuallyExclusive(obj_prices.prices, obj_prices.returns)
我有一个 class,它有两个互斥的参数(prices
和 returns
)。也就是说,不能为了实例化一个对象而同时提供它们。
但是,class 需要两者进行内部计算。所以我想根据用户提供的 pd.Series
计算缺失的
我创建了两个可供选择的 class 构造函数(from_prices
和 from_returns
)。使用这些构造函数,class 将被正确实例化。
这是代码。它使用 attrs
库 (www.attrs.org).
import pandas as pd
import attr
@attr.s
class MutuallyExclusive:
prices: pd.Series = attr.ib()
returns: pd.Series = attr.ib()
trading_days_per_year: int = attr.ib(default=252)
@classmethod
def from_prices(cls, price_series: pd.Series, trading_days: int = 252):
return cls(
price_series,
price_series.pct_change(),
trading_days,
)
@classmethod
def from_returns(cls, return_series: pd.Series):
return cls(
pd.Series(data=100 + 100 * (returns.add(1).cumprod() - 1)),
return_series,
)
if __name__ == "__main__":
prices = pd.Series(data=[100, 101, 98, 104, 102, 108])
returns = pd.Series(data=[0.01, 0.03, -0.02, 0.01, -0.03, 0.04])
obj_returns = MutuallyExclusive.from_returns(returns)
obj_prices = MutuallyExclusive.from_prices(prices, trading_days=100)
但是,尽管这两个系列彼此不兼容,用户仍然可以调用 obj = MutuallyExclusive(prices, returns)
。捕捉这种情况并抛出错误的最佳方法是什么?
编辑:
是否可以一起“禁用”常规构造函数?如果可以仅通过替代构造函数实例化对象,这将解决问题,不是吗?
您在 __attrs_post_init__
中检查:https://www.attrs.org/en/stable/init.html#post-init
attrs
库是正确的工具吗?
为什么不使用常规 python class 并自己定义 __init__()
?
import pandas as pd
class MutuallyExclusive:
def __init__(self, prices: pd.Series = None, returns: pd.Series = None):
if prices is not None and returns is not None:
raise ValueError("prices and returns are mutually exclusive")
self.prices = prices if prices is not None else pd.Series(data=100 * (1 + returns))
self.returns = returns if returns is not None else prices.pct_change()
if __name__ == "__main__":
prices = pd.Series(data=[100, 101, 98, 104, 102, 108])
returns = pd.Series(data=[0.01, 0.03, -0.02, 0.01, -0.03, 0.04])
obj_returns = MutuallyExclusive(returns=returns)
obj_prices = MutuallyExclusive(prices=prices)
编辑:您更新了您的示例,所以我的回答缺少 trading_days_per_year
但概念是相同的。
如果你想使用 attrs
库,其他人已经指出你可以将你的逻辑放在 __attrs_post_init__
函数中,请参见下面的示例删除对 class 的需要方法
请注意,您需要将价格和 returns 都默认为 None
def __attrs_post_init__(self):
if self.prices is not None and self.returns is not None:
raise ValueError("prices and returns are mutually exclusive")
if self.returns is None:
self.returns = self.price_series.pct_change()
if self.prices is None:
self.prices = pd.Series(data=100 + 100 * (self.returns.add(1).cumprod() - 1))
我不知道是否有更惯用的模式,但你可以用布尔锁保护构造函数,检查 __attrs_post_init__
中的锁以防止直接调用构造函数:
import pandas as pd
import attr
@attr.s
class MutuallyExclusive:
prices: pd.Series = attr.ib()
returns: pd.Series = attr.ib()
def __attrs_post_init__(self):
if not MutuallyExclusive.constructor_unlocked:
raise TypeError('Please use the `from_prices` or `from_returns` constructor methods')
@classmethod
def from_prices(cls, price_series: pd.Series):
cls.constructor_unlocked = True
value = cls(price_series, price_series.pct_change())
cls.constructor_unlocked = False
return value
@classmethod
def from_returns(cls, return_series: pd.Series):
cls.constructor_unlocked = True
value = cls(pd.Series(data=100 * (1 + return_series)), return_series)
cls.constructor_unlocked = False
return value
MutuallyExclusive.constructor_unlocked = False
if __name__ == "__main__":
prices = pd.Series(data=[100, 101, 98, 104, 102, 108])
returns = pd.Series(data=[0.01, 0.03, -0.02, 0.01, -0.03, 0.04])
obj_returns = MutuallyExclusive.from_returns(returns)
obj_prices = MutuallyExclusive.from_prices(prices)
bad = MutuallyExclusive(obj_prices.prices, obj_prices.returns)
或者,如果您愿意,threading.Lock
:
import pandas as pd
import attr
from threading import Lock
mutually_exclusive_constructor_lock = Lock()
@attr.s
class MutuallyExclusive:
prices: pd.Series = attr.ib()
returns: pd.Series = attr.ib()
def __attrs_post_init__(self):
if not mutually_exclusive_constructor_lock.locked():
raise TypeError('Please use the `from_prices` or `from_returns` constructor methods')
@classmethod
def from_prices(cls, price_series: pd.Series):
with mutually_exclusive_constructor_lock:
return cls(price_series, price_series.pct_change())
@classmethod
def from_returns(cls, return_series: pd.Series):
with mutually_exclusive_constructor_lock:
return cls(pd.Series(data=100 * (1 + return_series)), return_series)
if __name__ == "__main__":
prices = pd.Series(data=[100, 101, 98, 104, 102, 108])
returns = pd.Series(data=[0.01, 0.03, -0.02, 0.01, -0.03, 0.04])
obj_returns = MutuallyExclusive.from_returns(returns)
obj_prices = MutuallyExclusive.from_prices(prices)
bad = MutuallyExclusive(obj_prices.prices, obj_prices.returns)