Принудительное именование параметров в Python

111

В Python у вас может быть определение функции:

def info(object, spacing=10, collapse=1)

который можно вызвать любым из следующих способов:

info(odbchelper)                    
info(odbchelper, 12)                
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

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

Проблема, с которой мы сталкиваемся, заключается в том, что по мере роста некоторых из наших более крупных функций люди могут добавлять параметры между spacingи collapse, что означает, что неверные значения могут поступать в параметры, которые не имеют имени. Вдобавок иногда не всегда ясно, что нужно ввести. Нам нужен способ заставить людей называть определенные параметры - не только стандарт кодирования, но в идеале флаг или плагин pydev?

так что в приведенных выше 4 примерах только последний прошел бы проверку, поскольку все параметры названы.

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

Марк Мэйо
источник

Ответы:

215

В Python 3 - Да, вы можете указать *в списке аргументов.

Из документов :

Параметры после «*» или «* идентификатора» являются параметрами, содержащими только ключевое слово, и могут передаваться только в качестве аргументов с использованием ключевого слова.

>>> def foo(pos, *, forcenamed):
...   print(pos, forcenamed)
... 
>>> foo(pos=10, forcenamed=20)
10 20
>>> foo(10, forcenamed=20)
10 20
>>> foo(10, 20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes exactly 1 positional argument (2 given)

Это также можно сочетать с **kwargs:

def foo(pos, *, forcenamed, **kwargs):
Эли Бендерский
источник
32

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

def foo(*, arg0="default0", arg1="default1", arg2="default2"):
    pass

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

def foo(**kwargs):
    pass

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

мартега
источник
11

Конечно, большинство языков программирования делают порядок параметров частью контракта вызова функции, но это не обязательно . С чего бы это? Итак, я понимаю вопрос, отличается ли Python от других языков программирования в этом отношении. Помимо других хороших ответов для Python 2, обратите внимание на следующее:

__named_only_start = object()

def info(param1,param2,param3,_p=__named_only_start,spacing=10,collapse=1):
    if _p is not __named_only_start:
        raise TypeError("info() takes at most 3 positional arguments")
    return str(param1+param2+param3) +"-"+ str(spacing) +"-"+ str(collapse)

Единственный способ, которым вызывающий абонент сможет предоставить аргументы spacingи collapseпозиционно (без исключения):

info(arg1, arg2, arg3, module.__named_only_start, 11, 2)

Соглашение об отказе от использования закрытых элементов, принадлежащих другим модулям, уже является очень простым в Python. Как и в случае с самим Python, это соглашение для параметров будет применяться только наполовину.

В противном случае вызовы должны были бы иметь форму:

info(arg1, arg2, arg3, spacing=11, collapse=2)

Вызов

info(arg1, arg2, arg3, 11, 2)

присвоит параметру значение 11 _pи исключение, вызванное первой инструкцией функции.

Характеристики:

  • Параметры перед _p=__named_only_startдопускаются позиционно (или по имени).
  • Параметры после _p=__named_only_startдолжны быть предоставлены только по имени (если не __named_only_startполучены и не использованы сведения о специальном контрольном объекте ).

Плюсы:

  • Параметры являются точными по количеству и значению (конечно, позднее, если также будут выбраны хорошие имена).
  • Если дозорный указан в качестве первого параметра, тогда все аргументы необходимо указать по имени.
  • При вызове функции можно переключиться в позиционный режим, используя объект-дозорный __named_only_startв соответствующей позиции.
  • Можно ожидать лучшую производительность, чем другие альтернативы.

Минусы:

  • Проверка происходит во время выполнения, а не во время компиляции.
  • Использование дополнительного параметра (но не аргумента) и дополнительной проверки. Небольшое снижение производительности по сравнению с обычными функциями.
  • Функциональность - это взлом без прямой поддержки языком (см. Примечание ниже).
  • При вызове функции можно переключиться в позиционный режим, используя контрольный объект __named_only_startв правильном положении. Да, это тоже можно рассматривать как профи.

Имейте в виду, что этот ответ действителен только для Python 2. Python 3 реализует аналогичный, но очень элегантный механизм с поддержкой языка, описанный в других ответах.

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

Марио Росси
источник
«Проверка происходит во время выполнения, а не во время компиляции». - Я думаю, что это верно для всех проверок аргументов функций. Пока вы на самом деле не выполните строку вызова функции, вы не всегда знаете, какая функция выполняется. Кроме того, +1 - это умно.
Эрик
@Eric: Я бы предпочел статическую проверку. Но вы правы: это был бы совсем не Python. Хотя это и не решающий момент, конструкция Python 3 "*" также динамически проверяется. Спасибо за ваш комментарий.
Марио Росси
Кроме того, если вы назовете переменную модуля _named_only_start, становится невозможным ссылаться на нее из внешнего модуля, что устраняет плюсы и минусы. (одиночные начальные подчеркивания в области видимости модуля являются частными, IIRC)
Эрик
Что касается наименования дозорного, мы также могли бы иметь и a, __named_only_startи a named_only_start(без начального подчеркивания), второй, чтобы указать, что именованный режим «рекомендуется», но не до уровня «активно продвигаемого» (поскольку один является публичным, а другого нет). Что касается «приватности» _namesначала с подчеркивания, она не очень сильно обеспечивается языком: ее можно легко обойти, используя определенные (не *) импорта или уточненные имена. Вот почему в некоторых документах Python предпочитают использовать термин «закрытый» вместо «частный».
Марио Росси
6

Вы можете сделать это способом, который работает как в Python 2, так и в Python 3 , сделав «фиктивный» аргумент первого ключевого слова со значением по умолчанию, которое не будет встречаться «естественно». Этому аргументу ключевого слова может предшествовать один или несколько аргументов без значения:

_dummy = object()

def info(object, _kw=_dummy, spacing=10, collapse=1):
    if _kw is not _dummy:
        raise TypeError("info() takes 1 positional argument but at least 2 were given")

Это позволит:

info(odbchelper)        
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

но нет:

info(odbchelper, 12)                

Если вы измените функцию на:

def info(_kw=_dummy, spacing=10, collapse=1):

тогда все аргументы должны иметь ключевые слова и info(odbchelper)больше не будут работать.

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

Таким образом, нет необходимости возвращаться к использованию def(**kwargs)и потере информации подписи в интеллектуальном редакторе. Ваш социальный контракт заключается в предоставлении определенной информации, заставляя (некоторые из них) требовать ключевые слова, порядок, в котором они представлены, стал неактуальным.

Антон
источник
2

Обновить:

Я понял, что использование **kwargsне решит проблему. Если ваши программисты изменяют аргументы функции по своему усмотрению, можно, например, изменить функцию на это:

def info(foo, **kwargs):

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

Все сводится к тому, что говорит Брайан.


(...) люди могут добавлять параметры между spacingи collapse(...)

В общем, при изменении функций новые аргументы всегда должны идти до конца. В противном случае это нарушит код. Должно быть очевидно.
Если кто-то изменит функцию так, что код сломается, это изменение должно быть отклонено.
(Как говорит Брайан, это похоже на контракт)

(...) иногда не всегда ясно, что нужно делать.

Посмотрев на сигнатуру функции (т.е. def info(object, spacing=10, collapse=1)), можно сразу увидеть, что каждый аргумент, не имеющий значения по умолчанию, является обязательным.
То , для чего нужен аргумент, должно быть указано в строке документации.


Старый ответ (сохранен для полноты) :

Вероятно, это не лучшее решение:

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

def info(**kwargs):
    ''' Some docstring here describing possible and mandatory arguments. '''
    spacing = kwargs.get('spacing', 15)
    obj = kwargs.get('object', None)
    if not obj:
       raise ValueError('object is needed')

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

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

Феликс Клинг
источник
3
Мне больше понравился твой старый ответ. Просто напишите комментарий, почему вы принимаете только ** kwargs в функции. В конце концов, любой может изменить что угодно в исходном коде - вам нужна документация, чтобы описать намерение и цель ваших решений.
Брэндон,
В этом ответе нет фактического ответа!
Фил
2

Аргументы только для ключевых слов python3 ( *) можно смоделировать в python2.x с помощью**kwargs

Рассмотрим следующий код python3:

def f(pos_arg, *, no_default, has_default='default'):
    print(pos_arg, no_default, has_default)

и его поведение:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 3 were given
>>> f(1, no_default='hi')
1 hi default
>>> f(1, no_default='hi', has_default='hello')
1 hi hello
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required keyword-only argument: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() got an unexpected keyword argument 'wat'

Это может быть смоделировано с помощью следующего, заметьте , я взял на себя смелость перехода TypeErrorк KeyErrorв случае «требуется названный аргумент», это не было бы слишком много работы , чтобы сделать это же типа исключений , а также

def f(pos_arg, **kwargs):
    no_default = kwargs.pop('no_default')
    has_default = kwargs.pop('has_default', 'default')
    if kwargs:
        raise TypeError('unexpected keyword argument(s) {}'.format(', '.join(sorted(kwargs))))

    print(pos_arg, no_default, has_default)

И поведение:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes exactly 1 argument (3 given)
>>> f(1, no_default='hi')
(1, 'hi', 'default')
>>> f(1, no_default='hi', has_default='hello')
(1, 'hi', 'hello')
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
KeyError: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in f
TypeError: unexpected keyword argument(s) wat

Рецепт одинаково работает и в python3.x, но его следует избегать, если вы используете только python3.x

Энтони Соттиле
источник
Ах, так kwargs.pop('foo')идиома Python 2? Мне нужно обновить свой стиль кодирования. Я все еще использовал этот подход в Python 3 🤔
Нил
0

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

def foo(**args):
   print args

foo(1,2) # Raises TypeError: foo() takes exactly 0 arguments (2 given)
foo(hello = 1, goodbye = 2) # Works fine.
Нуфаль Ибрагим
источник
1
Вам нужно не только добавить проверки ключевых слов, но и подумать о потребителе, который знает, что он должен вызвать метод с подписью foo(**kwargs). Что я на это скажу? foo(killme=True, when="rightnowplease")
Dagrooms
Это действительно зависит от обстоятельств. Посмотрим dict.
Нуфал Ибрагим
0

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

Если вы все еще хотите это сделать, используйте декоратор функций и функцию inspect.getargspec . Это будет использоваться примерно так:

@require_named_args
def info(object, spacing=10, collapse=1):
    ....

Реализация require_named_argsоставлена ​​в качестве упражнения для читателя.

Я бы не стал заморачиваться. Каждый раз при вызове функции он будет медленным, и вы получите лучшие результаты, если будете писать код более тщательно.

Дэниел Ньюби
источник
-1

Вы можете использовать **оператор:

def info(**kwargs):

таким образом люди вынуждены использовать именованные параметры.

Оливье Вердье
источник
2
И понятия не имею, как вызвать свой метод, не читая код, увеличивая когнитивную нагрузку на потребителя :(
Dagrooms
По указанной причине это действительно плохая практика, и ее следует избегать.
Дэвид С.
-1
def cheeseshop(kind, *arguments, **keywords):

в python, если используется * args, это означает, что вы можете передать n-число аргументов для этого параметра - это будет список внутри функции для доступа

и если использовать ** kw, что означает его аргументы ключевого слова, которые могут быть доступны как dict - вы можете передать n-число аргументов kw, и если вы хотите ограничить, что пользователь должен ввести последовательность и аргументы по порядку, тогда не используйте * и ** - (его питонический способ предоставить общие решения для больших архитектур ...)

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

def info(object, spacing, collapse)
  spacing = spacing or 10
  collapse = collapse or 1
Шахджапан
источник
что произойдет, если интервал будет равен 0? (ответьте, получите 10). Этот ответ так же неверен, как и все другие ответы ** kwargs по тем же причинам.
Фил
-2

Я не понимаю, зачем программист вообще добавляет параметр между двумя другими.

Если вы хотите, чтобы параметры функции использовались с именами (например info(spacing=15, object=odbchelper)), тогда не имеет значения, в каком порядке они определены, поэтому вы также можете поместить новые параметры в конец.

Если вы действительно хотите, чтобы порядок имел значение, не ожидайте, что что-то сработает, если вы его измените!

Уманг
источник
2
Это не отвечает на вопрос. Неважно, хорошая это идея или нет - кто-то все равно может это сделать.
Грэм Перроу
1
Как сказал Грэм, кто-то все равно это сделает. Кроме того, если вы пишете библиотеку для использования другими, принудительная (только для Python 3) передача аргументов, содержащих только ключевые слова, обеспечивает дополнительную гибкость, когда вам нужно реорганизовать свой API.
s0undt3ch 04