【问题标题】:Is there a way to force mutually exclusive function parameters in python?有没有办法在python中强制互斥函数参数?
【发布时间】:2019-06-26 11:08:27
【问题描述】:

考虑:

def foobar(*, foo, bar):
    if foo:
        print('foo', end="")
    if bar:
        print('bar', end="")
    if foo and bar:
        print('No bueno', end='')  # I want this to be impossible
    if not foo and not bar:
        print('No bueno', end='')  # I want this to be impossible
    print('')


foobar(foo='bar')  # I want to pass inspection
foobar(bar='foo')  # I want to pass inspection
foobar(foo='bar', bar='foo')  # I want to fail inspection
foobar()  # I want to fail inspection

有没有办法设置一个函数,这样调用它的方式只有在传递 foo 或 bar 之一时才通过检查,而不需要在函数内部手动检查?

【问题讨论】:

  • foobarfoobar 定义中是关键字参数,不是可选参数。 (我见过很多人犯了相反的错误,但这是我第一次看到有人在这个方向上混淆。)
  • 是的,我知道它们现在不是可选的。我想知道是否有一种方法可以在调用foobar 时通过检查,您只能通过 foo 或 bar 中的一个。
  • 将它们都命名为 none 并检查?
  • 虽然这会使其失败,但它仍然可以通过检查,因此在运行代码之前有人不会意识到有问题。这就是我的来历。
  • “仍然通过检查”是什么意思?你想通过什么样的检查来抓住这个?一些特定的自动化工具?由具有一定 Python 经验水平的程序员进行代码审查?

标签: python python-3.x optional-parameters keyword-argument optional-arguments


【解决方案1】:

语法上没有。然而,使用装饰器相对容易做到这一点:

from functools import wraps

def mutually_exclusive(keyword, *keywords):
    keywords = (keyword,)+keywords
    def wrapper(func):
        @wraps(func)
        def inner(*args, **kwargs):
            if sum(k in keywords for k in kwargs) != 1:
                raise TypeError('You must specify exactly one of {}'.format(', '.join(keywords)))
            return func(*args, **kwargs)
        return inner
    return wrapper

用作:

>>> @mutually_exclusive('foo', 'bar')
... def foobar(*, foo=None, bar=None):
...     print(foo, bar)
... 
>>> foobar(foo=1)
1 None
>>> foobar(bar=1)
None 1
>>> foobar(bar=1, foo=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in inner
TypeError: You must specify exactly one of foo, bar
>>> foobar()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in inner
TypeError: You must specify exactly one of foo, bar

装饰器忽略未包含在给定列表中的位置和关键字参数:

>>> @mutually_exclusive('foo', 'bar')
... def foobar(a,b,c, *, foo=None, bar=None, taz=None):
...     print(a,b,c,foo,bar,taz)
... 
>>> foobar(1,2,3, foo=4, taz=5)
1 2 3 4 None 5
>>> foobar(1,2,3, foo=4, bar=5,taz=6)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in inner
TypeError: You must specify exactly one of foo, bar

如果参数可能是“可选的”(即,您最多可以指定其中一个关键字参数,但您也可以省略所有参数)只需将!= 1 更改为&lt;= 1in (0,1) 即可。

如果您将1 替换为数字k,则您可以概括装饰器以完全(或最多)接受您提供的集合中的指定参数k

无论如何,这对 PyCharm 没有任何帮助。据我所知,目前根本不可能告诉 IDE 你想要什么。


上面的装饰器有一个小“错误”:它认为foo=None 好像你传递了一个foo 的值,因为它出现在kwargs 列表中。通常你会期望传递默认值的行为应该与你根本没有指定参数一样。

正确解决此问题需要检查 wrapper 内的 func 以查找默认值并将 k in keywords 更改为 k in keywords and kwargs[k] != defaults[k] 之类的内容。

【讨论】:

  • 这是一种非常pythonic的方式来达到预期的结果。虽然我建议ValueErrorRuntimeError 代替TypeError。个人喜好,我想。
  • @PMende 我选择了TypeError,因为通常在您提供错误参数时引发的错误引发TypeError(例如def f(a): pass 然后f(b=1) 引发TypeError: f() got an unexpected keyword argument 'b'。所以我相信TypeError在这种情况下更加一致,因为我们正在为函数模拟一个特殊的签名......但这是一种观点。
  • 这也适用于类的构造函数吗?想想圆形类的典型示例,该类的对象将使用中点和(仅)半径或直径来生成......
  • 对于 python 数据类?
  • @GertVdE 您可以手动定义__init__ 并将装饰器放在上面(假设您使用相互排斥的参数,您可能还需要一些其他自定义逻辑)。如果您不想编写自己的自定义__init__,您可能必须使用元类。你应该查找一些关于 python3 元类的教程。
【解决方案2】:

简而言之:不,你不能那样做。

最接近的可能是使用断言:

def foobar(foo=None, bar=None):
    assert bool(foo) != bool(bar)

foobar(foo='bar')             # Passes
foobar(bar='foo')             # Passes
foobar(foo='bar', bar='foo')  # Raises an AssertionError
foobar()                      # Raises an AssertionError

bool 转换和!= 的组合将产生逻辑异或。

但要小心断言;他们可以被禁用。如果仅在开发期间需要您的检查也没关系。

【讨论】:

  • 小心检查布尔相等而不是is None。如果只传递了一个错误参数,这仍然会引发异常。
【解决方案3】:

标准库对此使用简单的运行时检查:

def foobar(*, foo=None, bar=None):
    if (foo is None) == (bar is None):
        raise ValueError('Exactly one of `foo` and `bar` must be provided')

【讨论】:

    【解决方案4】:

    您可以稍微重构一下并采用 两个 非可选参数,它们一起提供 一个 值:

    def foobar(name, value):
        if name == 'foo':
            foo = value
        elif name == 'bar':
            bar = value
        else:
            raise ValueError()
    

    这样就不可能传递两个 foo 或 bar 值。如果您添加了额外的参数,PyCharm 也会警告您。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-11-14
      • 1970-01-01
      • 2018-07-25
      • 1970-01-01
      • 2010-09-19
      • 2020-07-14
      • 2021-12-20
      • 1970-01-01
      相关资源
      最近更新 更多