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

Tob*_*ler 12 python arguments optional-parameters

举一个简单的例子,取一个可以返回其属性的class 椭圆,例如面积A,周长C,长轴/短轴a/b,偏心率e等.为了得到它,显然必须提供其两个参数以获得所有其他参数,虽然作为一个特殊情况,只提供一个参数应该假设一个圆圈.三个或更多一致的参数应该产生警告但是有效,否则显然会引发异常.

所以有效Ellipses的一些例子是:

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
Run Code Online (Sandbox Code Playgroud)

而无效的将是

Ellipse()
Ellipse(a=3, b=3, A=7)
Run Code Online (Sandbox Code Playgroud)

因此构造函数要么包含许多=None参数,

class Ellipse(object):
    def __init__(self, a=None, b=None, A=None, C=None, ...):
Run Code Online (Sandbox Code Playgroud)

或者,可能更明智,一个简单的**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})
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好.但现在实际实现,即确定提供哪些参数,哪些参数不是,并根据它们确定所有其他参数,或在需要时检查一致性.

我的第一种方法是许多人的简单而乏味的组合

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 ...
Run Code Online (Sandbox Code Playgroud)

等等*.但是没有更好的方法吗?或者这个类设计完全是bollocks,我应该创建构造函数Ellipse.create_from_a_b(a, b),尽管基本上使"提供三个或更多一致参数"选项不可能?

额外的问题:由于椭圆的周长涉及椭圆积分(或椭圆函数,如果提供圆周并且要获得其他参数),这些不完全是计算上的微不足道的,那么这些计算实际上应该在构造函数中还是放入@property Ellipse.C


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

Łuk*_*ski 15

我的建议主要集中在数据封装和代码可读性上.

a)在不明确的测量上选择对以在内部表示椭圆

class Ellipse(object):
    def __init__(a, b):
        self.a = a
        self.b = b
Run Code Online (Sandbox Code Playgroud)

b)创建属性族以获得关于椭圆的所需度量

class Ellipse(object):
    @property
    def area(self):
        return math.pi * self._x * self._b
Run Code Online (Sandbox Code Playgroud)

c)使用不明确的名称创建工厂类/工厂方法:

class Ellipse(object):
    @classmethod
    def fromAreaAndCircumference(cls, area, circumference):
        # convert area and circumference to common format
        return cls(a, b)
Run Code Online (Sandbox Code Playgroud)

样品用法:

ellipse = Ellipse.fromLongAxisAndEccentricity(axis, eccentricity)
assert ellipse.a == axis
assert ellipse.eccentricity == eccentricity
Run Code Online (Sandbox Code Playgroud)

  • 你的`fromAreaAndCircumference()`应该是`@ classmethod`而不是`@ staticmethod`(因为类可以被子类化,你想要实例化正确的类型)但是我同意,这是正确的设计. (4认同)

Jan*_*gen 7

  1. 检查您是否有足够的参数
  2. a从其他参数的每个配对计算
  3. 确认每一个a都是一样的
  4. 计算b从每一个配对a和其他参数
  5. 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)
Run Code Online (Sandbox Code Playgroud)

它很漂亮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 = ...
Run Code Online (Sandbox Code Playgroud)


bag*_*rat 6

如果只需要这个功能,我的建议是使用Nsh的答案,使用你提到的第二个解决方案.

否则,如果您的项目中出现了这个问题,我想出了一个解决方案:

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
Run Code Online (Sandbox Code Playgroud)

这将使您的代码更具可读性,并且您将隐藏此装饰器背后的丑陋细节,这些都是不言自明的.

注意:我们也可以删除@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
Run Code Online (Sandbox Code Playgroud)


lje*_*ibo 5

这是我的尝试.如果您正在为某些最终用户执行此操作,则可能需要跳过.我所做的可能很适合设置一些快速数学对象库,但只有当用户知道发生了什么时.

想法是所有描述数学对象的变量都遵循相同的模式,a = something*smntng.

因此,在计算变量irl时,在最坏的情况下我会丢失"某事",然后我会去计算那个值,并且在计算那个值时我会丢失任何值,并将其带回来完成计算我正在寻找的原始变量.有一定的递归模式值得注意.

因此,当计算变量时,在每次访问变量时,我必须检查它是否存在,以及它是否不计算它.因为我必须使用每次访问__getattribute__.

我还需要变量之间的函数关系.所以我将固定一个类属性relations,它将用于此目的.它将是变量和适当函数的字典.

但是如果我有所有必要的变量来计算当前的变量,我还必须提前检查.所以我将修改我的表,变量之间的集中数学关系,列出所有依赖项,在我去计算任何东西之前,我将运行列出的依赖项,并在我需要时计算它们.

所以现在它看起来更像是我们将进行半递归的乒乓匹配,其中函数_calc将调用__getattribute__哪些函数_calc再次调用.直到我们用完变量或实际计算出某些东西.

好的:

  • 没有ifs
  • 可以使用不同的init变量进行初始化.只要发送的变量能够计算其他变量.
  • 它相当通用,看起来它可以用于以类似方式描述的任何其他数学对象.
  • 计算完成后,您将记住所有变量.

:

  • 对于这个词对你来说意味着什么(显然总是更好),这是相当"非语言"的.
  • 用户不友好.您收到的任何错误消息都会与次数相同__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__都必须将所有属性设置为None,否则_getattr将调用a.

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

这里还有一个问题,与"坏"中的倒数第二点有关.也就是说,我们可以想象我们可以用C和定义一个椭圆A.因为我们可以与他人每个变量在只有1个函数依赖,如果你定义的变量a,并bea|b像我有,你将无法计算它们.始终至少会有一些微型的变量子集需要发送.这可以通过确保尽可能多地定义您的变量来减轻这些变量,尽可能少但是无法避免.

如果你很懒,这是一个很好的方法来短路你需要快速完成的事情,但我不会在某个地方这样做,我希望别人可以使用它!


101*_*101 4

对于奖励问题,根据请求进行计算可能是明智的(取决于您的用例),但如果之前计算过,请记住计算值。例如

@property
def a(self):
    return self._calc_a()

def _calc_a(self):
    if self.a is None:
        self.a = ...?
    return self.a
Run Code Online (Sandbox Code Playgroud)