Let's say I have a class AmbiguousClass
which has two attributes, a
and b
(let's say they are both int
, but it could be more general). They are related by some invertible equation, so that I can calculate a from b and reciprocally.
I want to give the user the possibility to instantiate an AmbiguousClass
by providing either a
or b
, depending on what is easier for them. Note that both ways to instantiate the variable can have the same signature, so this is not the typical polymorphism example. Providing none or both should result in an error/warning.
My best guess was something like this:
class AmbiguousClass():
def __init__(self, a=None, b=None):
#if no parameter is provided, we raise an error (can not instantiate)
if a is None and b is None:
raise SomeCustomError()
#if both a and b are provided, this seems redundant, a warning is raised
elif a is not None and b is not None:
warnings.warn(f"are you sure that you need to specify both a and b?")
self.a = a
self.b = b
# if a is provided, we calculate b from it
elif a is not None:
self.a = a
self.b = calculateB(self.a)
# if b is provided:
elif b is not None:
self.b =b
self.a = calculateA(self.b)
And then, the user would have to instantiate the class by specifying which keyword he is providing:
var1, var2 = AmbiguousClass(a=3), AmbiguousClass(b=6)
However, this feels a bit clunky, especially if the user decides to provide an argument without providing a keyword (it will by default be a
, but that is not clear from the user's point of view and might lead to unexpected behaviors).
How can I do this within the __init__
function in a way that is clear and will avoid unexpected behavior?
I would enforce using custom constructors that only take a single parameter and clearly indicate which parameter is provided, thus avoiding ambiguity.
class AmbiguousClass:
def __init__(self, a, b, _is_from_cls=False):
if not _is_from_cls:
raise TypeError(
"Cannot instantiate AmbiguousClass directly."
" Use classmethods 'from_a' or 'from_b' instead."
)
self.a = a
self.b = b
@classmethod
def from_a(cls, a):
b = calculateB(a)
return cls(a=a, b=b, _is_from_cls=True)
@classmethod
def from_b(cls, b):
a = calculateA(b)
return cls(a=a, b=b, _is_from_cls=True)
var1 = AmbiguousClass.from_a(a=3)
var2 = AmbiguousClass.from_b(b=6)
var3 = AmbiguousClass(3, 6) # will raise a TypeError
var4 = AmbiguousClass(3, 6, True) # will not raise a TypeError but the user explicitly overwrites the default parameter.