Есть ли способ заставить параметры взаимоисключающих функций в Python? - PullRequest
0 голосов
/ 01 февраля 2019

Рассмотрим:

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 ]

0 голосов
/ 02 февраля 2019

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

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

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

0 голосов
/ 02 февраля 2019

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

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

0 голосов
/ 02 февраля 2019

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

def foobar(*, foo=None, bar=None):
    if (foo is None) == (bar is None):
        raise ValueError('Exactly one of `foo` and `bar` must be provided')
0 голосов
/ 02 февраля 2019

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

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

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.

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

...