Есть ли способ заставить параметры взаимоисключающих функций в Python?

Рассматривать:

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 без проверки вручную внутри функции?

4 ответа

Синтаксически нет. Однако это сделать относительно легко, используя декоратор:

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 в <= 1 или же in (0,1) как ты предпочитаешь.

Если вы замените 1 с номером k Вы обобщаете декоратор, чтобы принять точно (или самое большее) k из указанных аргументов из предоставленного вами набора.

Это, однако, не поможет PyCharm в любом случае. Насколько я знаю в настоящее время просто невозможно сказать IDE, что вы хотите.


Вышеуказанный декоратор имеет небольшую "ошибку": он считает foo=None как будто вы передали значение для foo так как он появляется в kwargs список. Обычно вы ожидаете, что передача значения по умолчанию должна вести себя идентично, как если бы вы вообще не указывали аргумент.

Чтобы исправить это правильно, потребуется проверить func внутри wrapper найти значения по умолчанию и изменить k in keywords с чем-то вроде k in keywords and kwargs[k] != defaults[k],

Стандартная библиотека использует простую проверку времени выполнения для этого:

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

Короче говоря: нет, вы не можете этого сделать.

Наиболее близким к этому может быть использование утверждения:

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 преобразования и тому != сделаю логический XOR.

Будьте осторожны с утверждениями, хотя; они могут быть отключены. Хорошо, если ваш чек требуется только во время разработки.

Вы можете немного изменить рефакторинг и взять два не необязательных параметра, которые вместе предоставляют одно значение:

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

Таким образом, невозможно передать два значения foo или bar. PyCharm также предупредит вас, если вы добавите дополнительные параметры.

Другие вопросы по тегам