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

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

def func(arg1, arg2, ..., argN=default)

Я видел некоторые исключения в пакете PyTorch. Например, мы можем найти эту проблему в torch.randint. Как показано, он имеет ведущий необязательный аргумент в своих позиционных аргументах! Как это возможно?

Docstring:
randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor

Как мы можем определить функцию, как описано выше?


person javadr    schedule 09.09.2020    source источник
comment
Поскольку вы, кажется, желаете фактической рабочей реализации: какое поведение при вызове вы ожидаете для этого? \* не является допустимым спецификатором подписи. Вы имели в виду только \ или *?   -  person MisterMiyagi    schedule 09.09.2020
comment
На самом деле, я понятия не имею, что такое \*! Я только что скопировал из документации torch.randint. Как вы упомянули, мне очень любопытна его реализация в Python.   -  person javadr    schedule 09.09.2020


Ответы (3)


Одна функция не может иметь только ведущие необязательные параметры:

8.6. Определения функций

[...] Если параметр имеет значение по умолчанию, все последующие параметры до «*» также должны иметь значение по умолчанию — это синтаксическое ограничение, которое не выражается грамматикой.

Обратите внимание, что это исключает параметры, содержащие только ключевые слова, которые никогда не получают аргументы по положению.


При желании такое поведение можно эмулировать, вручную реализовав аргумент для сопоставления параметров. Например, можно отправлять на основе арности или явно сопоставлять вариативные аргументы.

def leading_default(*args):
    # match arguments to "parameters"
    *_, low, high, size = 0, *args
    print(low, high, size)

leading_default(1, 2)     # 0, 1, 2
leading_default(1, 2, 3)  # 1, 2, 3

Простая форма диспетчеризации обеспечивает перегрузку функций путем повторения сигнатур и вызова первой совпадающей.

import inspect


class MatchOverload:
    """Overload a function via explicitly matching arguments to parameters on call"""
    def __init__(self, base_case=None):
        self.cases = [base_case] if base_case is not None else []

    def overload(self, call):
        self.cases.append(call)
        return self

    def __call__(self, *args, **kwargs):
        failures = []
        for call in self.cases:
            try:
                inspect.signature(call).bind(*args, **kwargs)
            except TypeError as err:
                failures.append(str(err))
            else:
                return call(*args, **kwargs)
        raise TypeError(', '.join(failures))


@MatchOverload
def func(high, size):
    print('two', 0, high, size)


@func.overload
def func(low, high, size):
    print('three', low, high, size)


func(1, 2, size=3)    # three 1 2 3
func(1, 2)            # two 0 1 2
func(1, 2, 3, low=4)  # TypeError: too many positional arguments, multiple values for argument 'low'
person MisterMiyagi    schedule 09.09.2020
comment
@MinsterMiyagi Спасибо. Но теперь мы не можем вызывать эту функцию с аргументами ключевого слова. Пожалуйста, взгляните на ответ AvivYaniv. Поскольку он упомянул некоторые части его реализации, кажется, что у него есть несколько определений. Но насколько я знаю, в питоне это невозможно. - person javadr; 09.09.2020
comment
@javadr Как уже упоминалось, можно выполнить отправку арности. Технически это тот же механизм, что и singledispatch — регистрация нескольких функций и вызов подходящей. Однако это требует точного соответствия желаемому поведению диспетчера, что не уточняется в вопросе. Обратите внимание, что, согласно подписи в вопросе, ни один из low, high или size не позволяет вызывать в качестве аргументов ключевого слова. Они только позиционные из-за предшествующего \. - person MisterMiyagi; 09.09.2020
comment
@javadr Я добавил пример отправки/перегрузки на основе подписи. Это то, что вы ищете? - person MisterMiyagi; 09.09.2020
comment
@MinsterMiyagi Хотя ваше решение - очень хорошая реализация, я предпочитаю то, что делает PEP-484. +1 - person javadr; 09.09.2020
comment
@javadr Пожалуйста, уточните. PEP 484 относится к подсказкам и аннотациям типов — typing.overload не имеет никакого поведения во время выполнения. На самом деле он ничего не перегружает, просто намекает на альтернативную сигнатуру для проверки статического типа. - person MisterMiyagi; 09.09.2020
comment
@javadr Я хорошо знаю, что это такое, особенно то, что на самом деле он ничего не перегружает. Как говорится в связанном PEP: определения, оформленные @overload, предназначены только для проверки типов, поскольку они будут перезаписаны определением, не оформленным @overload, в то время как последнее используется во время выполнения, но должно игнорироваться средством проверки типов . - person MisterMiyagi; 09.09.2020

Ваше открытие очаровало меня, так как в Python (и во всех других языках, которые я знаю) действительно незаконно иметь ведущие необязательные аргументы, которые наверняка вызовут в нашем случае:

SyntaxError: non-default argument follows default argument

У меня возникли подозрения, но я искал исходный код:

Я нашел в строках 566-596 TensorFactories.cpp что на самом деле существует несколько (!) реализаций randint:

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ randint ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Tensor randint(int64_t high, IntArrayRef size, const TensorOptions& options) {
  return native::randint(high, size, c10::nullopt, options);
}

Tensor randint(
    int64_t high,
    IntArrayRef size,
    c10::optional<Generator> generator,
    const TensorOptions& options) {
  return native::randint(0, high, size, generator, options);
}

Tensor randint(
    int64_t low,
    int64_t high,
    IntArrayRef size,
    const TensorOptions& options) {
  return native::randint(low, high, size, c10::nullopt, options);
}

Tensor randint(
    int64_t low,
    int64_t high,
    IntArrayRef size,
    c10::optional<Generator> generator,
    const TensorOptions& options) {
  auto result = at::empty(size, options);
  return result.random_(low, high, generator);
}

Этот шаблон повторяется в строках 466–471 документа gen_pyi.py. где он генерирует сигнатуры типов для функций верхнего уровня:

        'randint': ['def randint(low: _int, high: _int, size: _size, *,'
                    ' generator: Optional[Generator]=None, {}) -> Tensor: ...'
                    .format(FACTORY_PARAMS),
                    'def randint(high: _int, size: _size, *,'
                    ' generator: Optional[Generator]=None, {}) -> Tensor: ...'
                    .format(FACTORY_PARAMS)],

Итак, что в основном происходит, так это то, что нет реального опционального параметра, а есть несколько функций, в которых один присутствует, а в другом его нет.

Это означает, что когда randint вызывается без параметра low, он устанавливается как 0:

Tensor randint(
    int64_t high,
    IntArrayRef size,
    c10::optional<Generator> generator,
    const TensorOptions& options) {
  return native::randint(0, high, size, generator, options);
}

Дальнейшее исследование, что касается запроса OP о том, как это возможно, что существует несколько функций с одним и тем же именем и разными аргументами:

Возвращаясь еще раз к gen_pyi.py, мы видим, что эти функции собираются в unsorted_function_hints, определенный в строке 436, затем он используется для создания function_hints в строках 509-513, и, наконец, function_hints устанавливается в env в строке 670.

Словарь env используется для записи файлов-заглушек pyi.

Эти файлы-заглушки используют перегрузку функций/методов, как описано в PEP-484.

Перегрузка функций/методов, используйте декоратор @overload:

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

Вот пример:

from typing import overload

class bytes:
    ...
    @overload
    def __getitem__(self, i: int) -> int: ...
    @overload
    def __getitem__(self, s: slice) -> bytes: ...

Итак, у нас есть определение одной и той же функции __getitem__ с разными аргументами.

И еще пример:

from typing import Callable, Iterable, Iterator, Tuple, TypeVar, overload

T1 = TypeVar('T1')
T2 = TypeVar('T2')
S = TypeVar('S')

@overload
def map(func: Callable[[T1], S], iter1: Iterable[T1]) -> Iterator[S]: ...
@overload
def map(func: Callable[[T1, T2], S],
        iter1: Iterable[T1], iter2: Iterable[T2]) -> Iterator[S]: ...
# ... and we could add more items to support more than two iterables

Здесь у нас есть определение той же функции map с другим количеством аргументов.

person Aviv Yaniv    schedule 09.09.2020
comment
Спасибо. Я видел эти определения, но не понял, как их реализовать на питоне! Как вы знаете, в Python не разрешается иметь уникальное имя для нескольких функций с разными типами аргументов, как это возможно в C/C++. - person javadr; 09.09.2020
comment
@javadr Спасибо за ваш ответ, подробно описывающий перегрузку функций/методов PEP-484 :) - person Aviv Yaniv; 09.09.2020
comment
Я хотел узнать PEP-484! :D +1 - person javadr; 09.09.2020
comment
Обратите внимание, что typing.overload не работает во время выполнения. Он просто существует как аннотация для использования средствами проверки статического типа. - person MisterMiyagi; 09.09.2020

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

У нас есть библиотека multipledispatch:

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

Итак, давайте использовать его:

from multipledispatch import dispatch

@dispatch(int, int)
def randint(low, high):
    my_randint(low, high)

@dispatch(int)
def randint(high):
    my_randint(0, high)

def my_randint(low, high):
    print(low, high)

# 0 5
randint(5)

# 2 3
randint(2, 3)
person Aviv Yaniv    schedule 09.09.2020
comment
@javadr Основываясь на результатах предыдущего исследования, вот элегантное решение о том, как добиться поведения, подобного ведущему необязательному аргументу. - person Aviv Yaniv; 09.09.2020
comment
Отлично и так просто в использовании. :D - person javadr; 09.09.2020