假设我有一个类
AmbiguousClass
,它有两个属性,a
和b
(假设它们都是int
,但它可以更通用)。它们通过一些可逆方程相关,这样我就可以从 b 计算 a 并计算出 a 的倒数。
我想让用户可以通过提供
AmbiguousClass
或 a
来实例化 b
,具体取决于对他们来说更容易的方式。请注意,实例化变量的两种方法可以具有相同的签名,因此这不是典型的多态性示例。不提供或两者都提供应该会导致错误/警告。
如何在
__init__
函数中执行此操作?
我最好的猜测是这样的:
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)
然后,用户必须通过指定他提供的关键字来实例化该类:
var1, var2 = AmbiguousClass(a=3), AmbiguousClass(b=6)
但是,这感觉有点笨拙,特别是如果用户决定提供参数而不提供关键字(默认情况下为
a
,但从用户的角度来看这并不明确,可能会导致意外行为)
我将强制使用仅采用单个参数的自定义构造函数,并清楚地指示提供了哪个参数,从而避免歧义。
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.
编辑:
添加了直接调用
AmbiguousClass(2,8)
时的错误。
您可以在构造函数中强制使用关键字。
这可能是一个偏好问题,但我喜欢尽可能多地进行“惰性”处理 - 即,在类构建期间进行最少的处理
类似这样的:
import warnings
class SomeCustomException(Exception):
...
class AmbiguousClass:
def __init__(self, *, a=None, b=None):
if a is None and b is None:
raise SomeCustomException('At least one parameter must be specified')
if a is not None and b is not None:
warnings.warn('Are you sure that you need to specify both a and b?')
self._a = a
self._b = b
def calculateA(self):
return 999
def calculateB(self):
return 666
@property
def a(self):
if self._a is None:
self._a = self.calculateA()
return self._a
@property
def b(self):
if self._b is None:
self._b = self.calculateB()
return self._b
print(AmbiguousClass(a=3).b)
更好的做法是从一开始就不允许出现任何歧义。也就是说,强制使用关键字参数,并禁止调用者同时提供两个参数——这根本没有充分的理由。
可以使用
*
语法强制执行关键字参数,而不允许提供两个参数可以通过使用布尔比较来完成:
class UnambiguousClass:
def __init__(self, *, a=None, b=None):
assert (a is None) != (b is None), "Exactly one of `a` and `b` must be provided."
if a is None:
self.a = calculateA(b)
self.b = b
else:
self.a = a
self.b = calculateB(a)
在设计像
AmbiguousClass
这样的类时,可以使用两个不同的属性(a
和b
)来完成实例化,同时确保不同时提供这两个属性,您确实可以实现更Pythonic和更清晰的实现。为此,我们可以利用类方法作为替代构造函数,允许用户更直观地提供 a
或 b
,而无需仅依赖关键字参数。
这是使用类方法的更 Pythonic 方法:
class AmbiguousClass():
def __init__(self, a=None, b=None):
# If no parameter is provided, raise an error (cannot instantiate)
if a is None and b is None:
raise ValueError("Either 'a' or 'b' must be provided.")
# If both a and b are provided, raise a warning
elif a is not None and b is not None:
warnings.warn("Are you sure that you need to specify both 'a' and 'b'?")
# If a is provided, calculate b from it
elif a is not None:
self.a = a
self.b = self.calculateB(self.a)
# If b is provided:
elif b is not None:
self.b = b
self.a = self.calculateA(self.b)
@classmethod
def from_a(cls, a):
# Instantiate using 'a'
return cls(a=a)
@classmethod
def from_b(cls, b):
# Instantiate using 'b'
return cls(b=b)
@staticmethod
def calculateB(a):
# Your implementation to calculate 'b' from 'a'
pass
@staticmethod
def calculateA(b):
# Your implementation to calculate 'a' from 'b'
pass
通过此实现,用户现在可以使用替代构造函数
AmbiguousClass
和 from_a()
实例化 from_b()
,这清楚地表明了它们提供的属性。当您提供参数而不指定关键字时,此方法有助于避免意外行为。
现在可以如何实例化该类:
# Using the from_a() constructor
var1 = AmbiguousClass.from_a(3)
# Using the from_b() constructor
var2 = AmbiguousClass.from_b(6)
这种方法提高了可读性并减少了无意错误的可能性,提供了一种更 Pythonic 和用户友好的方式来使用
AmbiguousClass
或 a
实例化 b
。