Принудительное использование именных аргументов

Одним из способов улучшить удобочитаемость кода является использование именных аргументов в функциях. В некоторых функциях необходимо, чтобы отдельные аргументы могли быть только именными. Как добиться такого поведения в Python 3 и Python 2?

Задача

Предположим, вы хотите разделить одно число на другое. В одном случае есть необходимость проигнорировать исключение ZeroDivisionError и возвратить бесконечность. В другом случае — проигнорировать исключение OverflowError и возвратить ноль.

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

def safe_division(number, divisor, ignore_overflow, 
                  ignore_zero_division):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

Этот вызов проигнорирует ошибку деления на ноль и возвратит ноль:

result = safe_division(1, 10**500, True, False)
print(result)
>>>
0.0

А такой вызов вернет бесконечность:

result = safe_division(1, 0, False, True)
print(result)
>>>
inf

Проблема в том, что легко спутать положение двух логических аргументов, которые контролируют поведение, исключающее игнорирование. Это может легко вызвать ошибки, которые трудно отслеживать.

Одним из способов улучшить удобочитаемость этого кода является использование именных аргументов.

def safe_division_b(number, divisor,
                    ignore_overflow=False,
                    ignore_zero_division=False):
    # ...

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

safe_division_b(1, 10**500, ignore_overflow=True)
safe_division_b(1, 0, ignore_zero_division=True)

Проблема в том, что именные аргументы в данном случае являются необязательными. Даже с новым определением safe_division_b, вы все равно можете вызвать эту функцию, используя только позиционные аргументы, что ухудшает ясность кода.

safe_division_b(1, 10**500, True, False)

Хорошая идея — обеспечить принудительное использование именных аргументов.

Решение для Python 3

Используйте символ *. В списке аргументов он укажет на окончание всех позиционных и начало только именнованных аргументов.

def safe_division_c(number, divisor, *,
                    ignore_overflow=False,
                    ignore_zero_division=False):
    # ...

Попробуем вызвать функцию, используя только позиционные аргументы:

safe_division_c(1, 10**500, True, False)
>>>
TypeError: safe_division_c() takes 2 positional arguments but 4 were given

Решение для Python 2

К сожалению, Python 2 не имеет явного синтаксиса для требования использовать только именные аргументы. Но вы можете добиться такого же поведения, если будете выбрасывать исключение TypeError для недопустимых вызовов функций, используя оператор ** в списках аргументов.

def safe_division_d(number, divisor, **kwargs):
    ignore_overflow = kwargs.pop('ignore_overflow', False)
    ignore_zero_div = kwargs.pop('ignore_zero_division', False)
    if kwargs:
        raise TypeError('Unexpected **kwargs: %r' % kwargs)
    # ...

Попробуем вызвать функцию, используя только позиционные аргументы:

safe_division_d(1, 0, False, True)
>>>
TypeError: safe_division_d() takes 2 positional arguments but 4 were given

Заметка подготовлена по материалам Бретта Сталкина.