如何pythonically有部分互斥的可选参数?

时间:2015-02-19 11:07:09

标签: python arguments optional-parameters

举一个简单的例子,选择可以返回其属性的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


* 我想至少有一个可读性改进总是提取ab并从中计算其余部分,但这意味着重新计算已经提供的值,浪费两者时间和精确度......

7 个答案:

答案 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)

  1. 检查您是否有足够的参数
  2. 从其他参数的每个配对计算a
  3. 确认每个a是相同的
  4. b和另一个参数
  5. 的每个配对中计算a
  6. 计算ab
  7. 中的其他参数

    这是一个简短版本,只有abef,可以轻松扩展到其他参数:

    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()行来使其更明确。

    我只对ef进行了编码,但它是可扩展的。只需将ef代码与您想要添加的任何方程(区域,周长等)模拟为ab的函数。

    我没有将您的单参数省略号请求包含在圈子中,但这只是在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
  • 可以使用不同的init变量进行初始化。只要发送的变量能够计算其他变量。
  • 它相当通用,看起来它可以用于以类似方式描述的任何其他数学对象。
  • 一旦计算完,您的所有变量都将被记住。

错误

  • 它是公平的" unpythonic"无论这个词对你意味着什么(明确总是更好)。
  • 不方便用户使用。您收到的任何错误消息都与__getattribute___calc相互呼叫的次数一样长。也没有很好的方法来制定漂亮的错误打印。
  • 您手头有一致性问题。这可以通过重写setter来处理。
  • 根据初始参数,您可能需要等待很长时间才能计算某个变量,尤其是在所请求的变量计算必须通过其他几个计算时。
  • 如果你需要一个复杂的功能,你必须确保在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中的倒数第二点相关;坏"。即让我们想象我们可以用CA定义一个椭圆。因为我们可以通过仅1个函数依赖关系将每个变量与其他变量相关联,如果您像我一样定义了变量ab而不是ea|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提供的以下显着差异的答案:

  • 关系在班级定义
  • 工作在定义时完成,以将它们置换为依赖集的规范参考,以及可用时计算的目标变量
  • 计算值被缓存但不要求实例是不可变的,因为存储的值可能无效(例如可以进行完全转换)
  • 基于非lambda的值计算,提供更多灵活性

我是从头开始写的,所以可能会有一些我错过的东西,但应该充分涵盖以下内容:

  • 定义数据依赖关系并拒绝初始化不充分的数据
  • 缓存计算结果以避免额外工作
  • 返回一个有意义的异常,其中包含不能从指定信息中导出的变量名称

当然,这可以分为基类来完成核心工作和子类,它只定义基本关系和计算。将扩展关系映射的逻辑拆分出子类可能是一个有趣的问题,因为关系必须在子类中指定。

编辑:重要的是要注意,该实现不拒绝不一致的初始化数据(例如,指定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_bAcheck()的用例需要一些其他行:

    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方法的顺序,这可能比编写几十个检查更易于管理。