举一个简单的例子,选择可以返回其属性的class
Ellipse,例如区域A
,周长C
,长轴/短轴a/b
,古怪e
等。为了得到这个,显然必须准确地提供其中的两个参数来获得所有其他参数,但是作为一个特殊情况,只提供一个参数应该假设一个圆。三个或更多一致的参数应该产生警告但是有效,否则显然会引发异常。
因此,有效Ellipse
的一些示例是:
Ellipse(a=5, b=2)
Ellipse(A=3)
Ellipse(a=3, e=.1)
Ellipse(a=3, b=3, A=9*math.pi) # note the consistency
而无效的
Ellipse()
Ellipse(a=3, b=3, A=7)
因此,构造函数将包含许多=None
个参数,
class Ellipse(object):
def __init__(self, a=None, b=None, A=None, C=None, ...):
或者,可能更明智,一个简单的**kwargs
,可能会添加提供a,b
作为位置参数的选项,
class Ellipse(object):
def __init__(self, a=None, b=None, **kwargs):
kwargs.update({key: value
for key, value in (('a', a), ('b', b))
if value is not None})
到目前为止,这么好。但现在实际实现了,即确定提供了哪些参数,哪些参数不是,并根据它们确定所有其他参数,或者在需要时检查一致性。
我的第一种方法是许多
的简单而乏味的组合if 'a' in kwargs:
a = kwargs['a']
if 'b' in kwargs:
b = kwargs['b']
A = kwargs['A'] = math.pi * a * b
f = kwargs['f'] = math.sqrt(a**2 - b**2)
...
elif 'f' in kwargs:
f = kwargs['f']
b = kwargs['b'] = math.sqrt(a**2 + f**2)
A = kwargs['A'] = math.pi * a * b
...
elif ...
依此类推 * 。但是没有更好的方法吗?或者这个类设计完全是bollocks,我应该创建像Ellipse.create_from_a_b(a, b)
这样的构造函数,尽管这基本上使"提供三个或更多一致的参数"选项不可能?
奖金问题:由于ellipse's circumference涉及椭圆积分(或椭圆函数,如果提供圆周并且要获得其他参数),这些计算不完全是计算上的,那么这些计算实际上应该在构造函数中还是而是放入@property Ellipse.C
?
* 我想至少有一个可读性改进总是提取a
和b
并从中计算其余部分,但这意味着重新计算已经提供的值,浪费两者时间和精确度......
答案 0 :(得分:15)
我的提案主要关注data encapsulation和代码可读性。
a)在不明确的测量上选择对以在内部表示椭圆
class Ellipse(object):
def __init__(a, b):
self.a = a
self.b = b
b)创建属性族以获得有关椭圆的所需指标
class Ellipse(object):
@property
def area(self):
return math.pi * self._x * self._b
c)使用不明确的名称创建工厂类/工厂方法:
class Ellipse(object):
@classmethod
def fromAreaAndCircumference(cls, area, circumference):
# convert area and circumference to common format
return cls(a, b)
样本用法:
ellipse = Ellipse.fromLongAxisAndEccentricity(axis, eccentricity)
assert ellipse.a == axis
assert ellipse.eccentricity == eccentricity
答案 1 :(得分:7)
a
a
是相同的b
和另一个参数a
a
和b
这是一个简短版本,只有a
,b
,e
和f
,可以轻松扩展到其他参数:
class Ellipse():
def __init__(self, a=None, b=None, e=None, f=None):
if [a, b, e, f].count(None) > 2:
raise Exception('Not enough parameters to make an ellipse')
self.a, self.b, self.e, self.f = a, b, e, f
self.calculate_a()
for parameter in 'b', 'e', 'f': # Allows any multi-character parameter names
if self.__dict__[parameter] is None:
Ellipse.__dict__['calculate_' + parameter](self)
def calculate_a(self):
"""Calculate and compare a from every pair of other parameters
:raises Exception: if the ellipse parameters are inconsistent
"""
a_raw = 0 if self.a is None else self.a
a_be = 0 if not all((self.b, self.e)) else self.b / math.sqrt(1 - self.e**2)
a_bf = 0 if not all((self.b, self.f)) else math.sqrt(self.b**2 + self.f**2)
a_ef = 0 if not all((self.e, self.f)) else self.f / self.e
if len(set((a_raw, a_be, a_bf, a_ef)) - set((0,))) > 1:
raise Exception('Inconsistent parameters')
self.a = a_raw + a_be + a_bf + a_ef
def calculate_b(self):
"""Calculate and compare b from every pair of a and another parameter"""
b_ae = 0 if self.e is None else self.a * math.sqrt(1 - self.e**2)
b_af = 0 if self.f is None else math.sqrt(self.a**2 - self.f**2)
self.b = b_ae + b_af
def calculate_e(self):
"""Calculate e from a and b"""
self.e = math.sqrt(1 - (self.b / self.a)**2)
def calculate_f(self):
"""Calculate f from a and b"""
self.f = math.sqrt(self.a**2 - self.b**2)
它很漂亮Pythonic,虽然__dict__
使用可能不是。 __dict__
方式行数较少,重复次数较少,但您可以通过将其分解为单独的if self.b is None: self.calculate_b()
行来使其更明确。
我只对e
和f
进行了编码,但它是可扩展的。只需将e
和f
代码与您想要添加的任何方程(区域,周长等)模拟为a
和b
的函数。
我没有将您的单参数省略号请求包含在圈子中,但这只是在calculate_a
开头检查是否只有一个参数,在这种情况下,a
应设置为使椭圆成为圆形(如果b
是唯一的那个,则应设置a
):
def calculate_a(self):
"""..."""
if [self.a, self.b, self.e, self.f].count(None) == 3:
if self.a is None:
# Set self.a to make a circle
else:
# Set self.b to make a circle
return
a_raw = ...
答案 2 :(得分:6)
如果仅对此单一课程需要此类功能,我的建议是使用Nsh's answer来使用您提到的第二种解决方案。
否则,如果您的项目中出现了这个问题,我提出了一个解决方案:
class YourClass(MutexInit):
"""First of all inherit the MutexInit class by..."""
def __init__(self, **kwargs):
"""...calling its __init__ at the end of your own __init__. Then..."""
super(YourClass, self).__init__(**kwargs)
@sub_init
def _init_foo_bar(self, foo, bar):
"""...just decorate each sub-init method with @sub_init"""
self.baz = foo + bar
@sub_init
def _init_bar_baz(self, bar, baz):
self.foo = bar - baz
这将使您的代码更具可读性,并且您将隐藏此装饰器背后的丑陋细节,这些都是不言自明的。
注意:我们也可以删除@sub_init
装饰器,但我认为这是将该方法标记为子初始化的唯一合法方式。否则,一个选项是同意在方法名称之前加上一个前缀,比如_init
,但我认为这是一个坏主意。
以下是实施:
import inspect
class MutexInit(object):
def __init__(self, **kwargs):
super(MutexInit, self).__init__()
for arg in kwargs:
setattr(self, arg, kwargs.get(arg))
self._arg_method_dict = {}
for attr_name in dir(self):
attr = getattr(self, attr_name)
if getattr(attr, "_isrequiredargsmethod", False):
self._arg_method_dict[attr.args] = attr
provided_args = tuple(sorted(
[arg for arg in kwargs if kwargs[arg] is not None]))
sub_init = self._arg_method_dict.get(provided_args, None)
if sub_init:
sub_init(**kwargs)
else:
raise AttributeError('Insufficient arguments')
def sub_init(func):
args = sorted(inspect.getargspec(func)[0])
self_arg = 'self'
if self_arg in args:
args.remove(self_arg)
def wrapper(funcself, **kwargs):
if len(kwargs) == len(args):
for arg in args:
if (arg not in kwargs) or (kwargs[arg] is None):
raise AttributeError
else:
raise AttributeError
return func(funcself, **kwargs)
wrapper._isrequiredargsmethod = True
wrapper.args = tuple(args)
return wrapper
答案 3 :(得分:5)
这是我的尝试。如果您为某些最终用户执行此操作,则可能需要跳过此操作。我所做的可能很适合设置一些快速数学对象库,但只有在用户知道发生了什么时才会这样做。
想法是所有描述数学对象的变量都遵循相同的模式,a = something * smntng。
因此,在计算变量irl时,在最坏的情况下,我会丢失"某些东西",然后我去计算那个值,并且我会丢失任何值。计算那个,并把它带回来完成我正在寻找的原始变量的计算。某种递归模式很明显。
因此,在计算变量时,在变量的每次访问时,我必须检查它是否存在,以及它是否计算它。由于它在每次访问时都必须使用__getattribute__
。
我还需要变量之间的函数关系。因此,我会固定一个类属性relations
,它将用于此目的。它将成为变量和适当函数的词汇。
但是如果我有所有必要的变量来计算当前的变量,我还要提前检查。所以我修改我的表,变量之间的集中数学关系,列出所有依赖项,在我去计算任何东西之前,我会运行列出的依赖项,并在需要时计算它们。
所以现在看起来更像是我们将进行半递归的乒乓匹配,其中函数_calc
将调用__getattribute__
再次调用函数_calc
。直到我们用完变量或实际计算出某些东西。
好:
if
s 错误:
__getattribute__
和_calc
相互呼叫的次数一样长。也没有很好的方法来制定漂亮的错误打印。 relations
之前声明它可能会使代码变得丑陋(也见最后一点)。我无法弄清楚如何让它们成为实例方法,而不是类方法或其他更全局的函数,因为我基本上覆盖了.
运算符。a
需要b
,需要e
再次需要a
并进入无限循环)。 relations
设置为dict
类型。这意味着,每个变量名称只有1个函数依赖项,在数学术语中并不一定正确。value = self.relations[var]["func"]( *[self.__getattribute__(x) for x in requirements["req"]] )
_calc
中调用__getattribute__
的行再次调用_calc
,或者如果存在该变量,则返回该值。同样在每个__init__
,您必须将所有属性设置为无,否则将调用_getattr
。
def cmplx_func_A(e, C):
return 10*C*e
class Elipse():
def __init__(self, a=None, b=None, **kwargs):
self.relations = {
"e": {"req":["a", "b"], "func": lambda a,b: a+b},
"C": {"req":["e", "a"], "func": lambda e,a: e*1/(a*b)},
"A": {"req":["C", "e"], "func": lambda e,C: cmplx_func_A(e, C)},
"a": {"req":["e", "b"], "func": lambda e,b: e/b},
"b": {"req":["e", "a"], "func": lambda e,a: e/a}
}
self.a = a
self.b = b
self.e = None
self.C = None
self.A = None
if kwargs:
for key in kwargs:
setattr(self, key, kwargs[key])
def __getattribute__(self, attr):
val = super(Elipse, self).__getattribute__(attr)
if val: return val
return self._calc(attr)
def _calc(self, var):
requirements = self.relations[var]
value = self.relations[var]["func"](
*[self.__getattribute__(x) for x in requirements["req"]]
)
setattr(self, var, value)
return value
Oputput:
>>> a = Elipse(1,1)
>>> a.A #cal to calculate this will fall through
#and calculate every variable A depends on (C and e)
20
>>> a.C #C is not calculated this time.
1
>>> a = Elipse(1,1, e=3)
>>> a.e #without a __setattribute__ checking the validity, there is no
3 #insurance that this makes sense.
>>> a.A #calculates this and a.C, but doesn't recalc a.e
30
>>> a.e
3
>>> a = Elipse(b=1, e=2) #init can be anything that makes sense
>>> a.a #as it's defined by relations dict.
2.0
>>> a = Elipse(a=2, e=2)
>>> a.b
1.0
这里还有一个问题,与#34中的倒数第二点相关;坏"。即让我们想象我们可以用C
和A
定义一个椭圆。因为我们可以通过仅1个函数依赖关系将每个变量与其他变量相关联,如果您像我一样定义了变量a
和b
而不是e
和a|b
,那么你赢了&#39 ; t能够计算出来。始终至少会有一些微型的变量子集需要发送。这可以通过确保尽可能少地定义您的变量来减轻,但可以避免其他变量。
如果你很懒,这是一个很好的方法来短路你需要快速完成的事情,但是我不会在某个地方这样做,我希望其他人一直使用它!
答案 4 :(得分:4)
对于奖金问题,根据请求计算可能是合理的(取决于您的使用案例),但如果之前已计算过,则记住计算值。 E.g。
@property
def a(self):
return self._calc_a()
def _calc_a(self):
if self.a is None:
self.a = ...?
return self.a
答案 5 :(得分:4)
下面是我之前用于部分数据依赖和结果缓存的方法。它实际上类似于@ljetibo提供的以下显着差异的答案:
我是从头开始写的,所以可能会有一些我错过的东西,但应该充分涵盖以下内容:
当然,这可以分为基类来完成核心工作和子类,它只定义基本关系和计算。将扩展关系映射的逻辑拆分出子类可能是一个有趣的问题,因为关系必须在子类中指定。
编辑:重要的是要注意,该实现不拒绝不一致的初始化数据(例如,指定a,b,c和A使得它不满足用于计算的相互表达式)。假设实例化器只应使用最小的有意义数据集。通过实例化时间评估所提供的kwargs之间的一致性,可以毫不费力地执行OP的要求。
import itertools
class Foo(object):
# Define the base set of dependencies
relationships = {
("a", "b", "c"): "A",
("c", "d"): "B",
}
# Forumulate inverse relationships from the base set
# This is a little wasteful but gives cheap dependency set lookup at
# runtime
for deps, target in relationships.items():
deps = set(deps)
for dep in deps:
alt_deps = deps ^ set([dep, target])
relationships[tuple(alt_deps)] = dep
def __init__(self, **kwargs):
available = set(kwargs)
derivable = set()
# Run through the permutations of available variables to work out what
# other variables are derivable given the dependency relationships
# defined above
while True:
for r in range(1, len(available) + 1):
for permutation in itertools.permutations(available, r):
if permutation in self.relationships:
derivable.add(self.relationships[permutation])
if derivable.issubset(available):
# If the derivable set adds nothing to what is already noted as
# available, that's all we can get
break
else:
available |= derivable
# If any of the variables are underivable, raise an exception
underivable = set(self.relationships.values()) - available
if len(underivable) > 0:
raise TypeError(
"The following properties cannot be derived:\n\t{0}"
.format(tuple(underivable))
)
# Store the kwargs in a mapping where we'll also cache other values as
# are calculated
self._value_dict = kwargs
def __getattribute__(self, name):
# Try to collect the value from the stored value mapping or fall back
# to the method which calculates it below
try:
return super(Foo, self).__getattribute__("_value_dict")[name]
except (AttributeError, KeyError):
return super(Foo, self).__getattribute__(name)
# This is left hidden but not treated as a staticmethod since it needs to
# be run at definition time
def __storable_property(getter):
name = getter.__name__
def storing_getter(inst):
# Calculates the value using the defined getter and save it
value = getter(inst)
inst._value_dict[name] = value
return value
def setter(inst, value):
# Changes the stored value and invalidate saved values which depend
# on it
inst._value_dict[name] = value
for deps, target in inst.relationships.items():
if name in deps and target in inst._value_dict:
delattr(inst, target)
def deleter(inst):
# Delete the stored value
del inst._value_dict[name]
# Pass back a property wrapping the get/set/deleters
return property(storing_getter, setter, deleter, getter.__doc__)
## Each variable must have a single defined calculation to get its value
## Decorate these with the __storable_property function
@__storable_property
def a(self):
return self.A - self.b - self.c
@__storable_property
def b(self):
return self.A - self.a - self.c
@__storable_property
def c(self):
return self.A - self.a - self.b
@__storable_property
def d(self):
return self.B / self.c
@__storable_property
def A(self):
return self.a + self.b + self.c
@__storable_property
def B(self):
return self.c * self.d
if __name__ == "__main__":
f = Foo(a=1, b=2, A=6, d=10)
print f.a, f.A, f.B
f.d = 20
print f.B
答案 6 :(得分:3)
每次设置参数时,我都会检查数据的一致性。
import math
tol = 1e-9
class Ellipse(object):
def __init__(self, a=None, b=None, A=None, a_b=None):
self.a = self.b = self.A = self.a_b = None
self.set_short_axis(a)
self.set_long_axis(b)
self.set_area(A)
self.set_maj_min_axis(a_b)
def set_short_axis(self, a):
self.a = a
self.check()
def set_long_axis(self, b):
self.b = b
self.check()
def set_maj_min_axis(self, a_b):
self.a_b = a_b
self.check()
def set_area(self, A):
self.A = A
self.check()
def check(self):
if self.a and self.b and self.A:
if not math.fabs(self.A - self.a * self.b * math.pi) <= tol:
raise Exception('A=a*b*pi does not check!')
if self.a and self.b and self.a_b:
if not math.fabs(self.a / float(self.b) - self.a_b) <= tol:
raise Exception('a_b=a/b does not check!')
主要:
e1 = Ellipse(a=3, b=3, a_b=1)
e2 = Ellipse(a=3, b=3, A=27)
第一个椭圆对象是一致的; set_maj_min_axis(1)
传递正常。
第二个不是; set_area(27)
失败,至少在指定的1e-9容差范围内,并引发错误。
修改1
a
方法中使用a_b
,A
和check()
的用例需要一些其他行:
if self.a and self.A and self.a_b:
if not math.fabs(self.A - self.a **2 / self.a_b * math.pi) <= tol:
raise Exception('A=a*a/a_b*pi does not check!')
if self.b and self.A and self.a_b:
if not math.fabs(self.A - self.b **2 * self.a_b * math.pi) <= tol:
raise Exception('A=b*b*a_b*pi does not check!')
主:
e3 = Ellipse(b=3.0, a_b=1.0, A=27)
一种可以说明的方法是将self.b = self.a / float(self.a_b)
直接计算到a_b
的集合方法中。由于您自己决定构造函数中set方法的顺序,这可能比编写几十个检查更易于管理。