【问题标题】:How to pythonically have partially-mutually exclusive optional arguments?如何在 python 上具有部分互斥的可选参数?
【发布时间】:2015-04-20 16:49:47
【问题描述】:

举个简单的例子,以classEllipse为例,它可以返回其属性,例如面积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

无效的会是

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 ...

等等*。但是没有更好的办法吗?还是这个类的设计完全是胡扯,我应该创建诸如Ellipse.create_from_a_b(a, b) 之类的构造函数,尽管这基本上使“提供三个或更多一致的参数”选项变得不可能?

奖励问题:由于ellipse's circumference 涉及椭圆积分(或椭圆函数,如果提供周长并且要获得其他参数),这些计算在计算上并不完全是微不足道的,如果这些计算实际上是在构造函数中,或者更确切地说是放入@property Ellipse.C?


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

【问题讨论】:

  • 严格来说,3个参数即使是一致的,也应该被认为是过度定义的。
  • @figs True... 我想还有更明智的例子,例如线性方程组可以被过度定义,但有一个独特的解决方案
  • 我会避免允许两个以上的参数。这需要您检查一致性,由于数值精度问题,这实际上很难在一般意义上正确执行。
  • 我们可以假设这个Ellipse 对象是不可变的吗?这简化了实现,因为改变现有Ellipse 对象的a 是不明确的——它会影响其他值,但很难猜测例如b 保持不变还是A 保持不变。我问是因为如果我们同意使用属性,那么我们需要考虑到它。对于“我猜至少有一个可读性改进是......浪费时间和精度” - 如果不计算和比较所有内容,就无法在过度定义的场景中保持一致性。
  • @TobiasKienzler:是的,但问题仍然存在,任何人想要提供的输入变量的 n-choose-k 可能组合中有多少?我要说的是,在大多数现实世界的环境中,你并不需要真正支持每一种可能的组合。你支持特定的组合,因为你有特定的原因。 (例如,如果您在围栏建筑环境中使用圆周,或者在地板涂漆环境中使用区域,您可能允许圆周作为输入,但允许某些输入的动机是代数关系之外的。)跨度>

标签: python arguments optional-parameters


【解决方案1】:

我的建议侧重于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

【讨论】:

  • 哦,我忘了补充一件重要的事情,主要是浮动相等。在您的示例中,Ellipse(a=3, b=3, area=9 * pi) 是正确的。那么,Ellipse(a=0.4, b=0.0182, area=pi * 0.0728) 呢? (0. * 0.0182 * pi) == (pi * 0.0728) 的计算结果为 False。这是您不想将重复数据保留在一个实例中的另一个原因。
  • 您的fromAreaAndCircumference() 应该是@classmethod 而不是@staticmethod(因为该类可以被子类化并且您想实例化正确的类型)但否则我同意,这是正确的设计.
  • 很抱歉没有给予全部赏金,这是一个忙碌的周末:/我想你的提议在大多数情况下是最好的,除非只有少数有效论据的排列
【解决方案2】:
  1. 检查是否有足够的参数
  2. 从其他参数的每一对中计算a
  3. 确认每个a都是一样的
  4. 从a 和另一个参数的每一对中计算b
  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)

它非常 Pythonic,尽管 __dict__ 的用法可能不是。 __dict__ 方式行数更少,重复性更少,但您可以通过将其拆分为单独的 if self.b is None: self.calculate_b() 行来使其更加明确。

我只编码了e 和f,但它是可扩展的。只需模仿e 和f 代码,将您想要添加的任何内容(面积、周长等)作为a 和b 的函数。

我没有包括您要求将单参数椭圆变成圆的请求,但这只是在calculate_a 开头检查是否只有一个参数,在这种情况下应设置a 以使椭圆一个圆(如果a是唯一的,则应设置b):

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 = ...

【讨论】:

  • 嗯,条件零的求和是避免许多eilfs 的有趣方法
【解决方案3】:

如果此类功能只需要这个类,我的建议是使用您提到的第二种解决方案,使用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

【讨论】:

  • 非常有趣的方法 :) 嗯,甚至可以更进一步,使用带有内省的metaclass 甚至消除装饰器...
  • @Tobias 感谢您的评论。确切地说,元类是一种选择,但为了代码的可读性,我使用了装饰器。
  • 通过放置一个显示“requires _args”的装饰器,您可以向读者/客户提示如果您不提供 required 将会发生某些事情 参数:)
  • @accept 装饰器确实如此,但 @requires 基本上是在 init_foo_bar(self, foo, bar) 等方法中重复命名参数
  • 你是对的,但这里的问题是获取参数的名称。您可以在运行时通过检查调用堆栈来获取它们,这只能在调用 sub-init 时完成。因此,您不会在__init__ 中获得有关参数的信息。
【解决方案4】:

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

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

因此,在计算变量 irl 时,在最坏的情况下,我会丢失“某些东西”,然后我会去计算该值,以及在计算该值时我会丢失的任何值,然后将其带回完成计算我正在寻找的原始变量。有一种明显的递归模式。

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

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

但我还必须提前检查我是否拥有所有必要的变量来计算当前变量。所以我会修改我的表格,变量之间的集中数学关系,列出所有依赖关系,在我计算任何东西之前,我会遍历列出的依赖关系,如果我需要也计算它们。

所以现在看起来更像是一场半递归的乒乓球比赛,其中函数_calc 将调用__getattribute__,它再次调用函数_calc。直到我们用完变量或者我们实际计算了一些东西。

善意:

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

坏人:

  • 无论这个词对您意味着什么,它都是相当“不符合 Python 的”(明确总是更好)。
  • 对用户不友好。您收到的任何错误消息都将与 __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。

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

输出:

>>> 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

这里还有一个问题,与“坏”中的倒数第二点有关。 IE。假设我们可以用C 和A 定义一个椭圆。因为我们只能通过 1 个函数依赖关系将每个变量与其他变量相关联,所以如果您像我一样在 e 和 a|b 上定义变量 a 和 b,您将无法计算它们。总会有至少一些你必须发送的变量的微型子集。这可以通过确保定义尽可能多的变量而不是尽可能少的其他变量来缓解,但不能避免。

如果你很懒惰,这是一种让你需要快速完成的事情短路的好方法,但我不会在我希望其他人使用它的地方这样做,永远!

【讨论】:

  • 相当优雅的方式恕我直言,“丑陋”的部分可以放入装饰器,metaclass 或至少是父类
  • @TobiasKienzler 是的,内部结构很好,但丑陋的无法避免在继承的类中,relation dict 的函数关系必须在__init__ 之前为defed。这意味着它们必须来自外部.py 文件并被包含(这是我通常所做的,应该外包大型函数(即>1k 行)以提高可读性)或字面意思被定义在__init__ 之前。你会同意这并不是真正被社会接受的蟒蛇。大多数其他问题可以通过仔细设置relations 和良好的 setattr 函数来解决。可选择使用validate 方法。
【解决方案5】:

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

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

def _calc_a(self):
    if self.a is None:
        self.a = ...?
    return self.a

【讨论】:

  • 好点。嗯,@property 可以与 @memoize 装饰器组合吗?我不会为您的帖子投票,以便将主要问题保留在“未回答”问题列表中
【解决方案6】:

下面是我之前用于部分数据依赖和结果缓存的一种方法。它实际上类似于@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

【讨论】:

    【解决方案7】:

    每次设置参数时,我都会检查数据的一致性。

    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

    在 check() 方法中使用 a、a_b 和 A 的情况需要一些额外的行:

        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 方法中。由于您自己决定了构造函数中设置方法的顺序,这可能比编写数十个检查更易于管理。

    【讨论】:

    • 如果椭圆仅指定了b 和A,这如何确定a?
    • 这里我给出一些关于设置/检查机制的想法。我还没有写get方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-07-25
    • 2017-01-17
    • 2013-05-23
    • 2014-01-18
    • 2020-07-14
    • 2021-11-21
    • 2012-12-04
    相关资源
    最近更新 更多