Nomenclatura forçada de parâmetros em Python

111

Em Python, você pode ter uma definição de função:

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

que pode ser chamado de qualquer uma das seguintes maneiras:

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

graças à permissão do Python de argumentos de qualquer ordem, desde que sejam nomeados.

O problema que estamos tendo é que, à medida que algumas de nossas funções maiores aumentam, as pessoas podem estar adicionando parâmetros entre spacinge collapse, o que significa que os valores errados podem estar indo para parâmetros que não são nomeados. Além disso, às vezes nem sempre está claro o que precisa ser inserido. Queremos uma maneira de forçar as pessoas a nomear certos parâmetros - não apenas um padrão de codificação, mas, de preferência, um sinalizador ou plugin pydev?

de modo que nos 4 exemplos acima, apenas o último passaria na verificação, pois todos os parâmetros são nomeados.

Provavelmente, vamos ativá-lo apenas para determinadas funções, mas quaisquer sugestões sobre como implementar isso - ou se é possível, seriam apreciadas.

Mark Mayo
fonte

Respostas:

214

Em Python 3 - Sim, você pode especificar *na lista de argumentos.

Dos documentos :

Os parâmetros após “*” ou “* identificador” são parâmetros apenas de palavra-chave e só podem ser passados ​​para argumentos de palavra-chave usados.

>>> 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)

Isso também pode ser combinado com **kwargs:

def foo(pos, *, forcenamed, **kwargs):
Eli Bendersky
fonte
32

Você pode forçar as pessoas a usar argumentos de palavra-chave em Python3 definindo uma função da seguinte maneira.

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

Ao tornar o primeiro argumento um argumento posicional sem nome, você força todos que chamam a função a usar os argumentos de palavra-chave, que é o que acho que você estava perguntando. Em Python2, a única maneira de fazer isso é definir uma função como esta

def foo(**kwargs):
    pass

Isso forçará o chamador a usar kwargs, mas essa não é uma solução tão boa, pois você teria que colocar uma marca para aceitar apenas o argumento de que precisa.

Martega
fonte
11

É verdade que a maioria das linguagens de programação torna a ordem dos parâmetros parte do contrato de chamada de função, mas não precisa ser assim. Por que seria? Meu entendimento da questão é, então, se Python é diferente de outras linguagens de programação nesse aspecto. Além de outras boas respostas para Python 2, considere o seguinte:

__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)

A única maneira que um chamador seria capaz de fornecer argumentos spacinge collapseposicionalmente (sem uma exceção) seria:

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

A convenção de não usar elementos privados pertencentes a outros módulos já é muito básica em Python. Como acontece com o próprio Python, essa convenção para parâmetros seria apenas parcialmente aplicada.

Caso contrário, as chamadas precisariam ser no formato:

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

Uma chamada

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

atribuiria o valor 11 ao parâmetro _pe uma exceção gerada pela primeira instrução da função.

Características:

  • Os parâmetros anteriores _p=__named_only_startsão admitidos posicionalmente (ou pelo nome).
  • Parâmetros depois _p=__named_only_start devem ser fornecidos apenas pelo nome (a menos que __named_only_startseja obtido e usado conhecimento sobre o objeto sentinela especial ).

Prós:

  • Os parâmetros são explícitos em número e significado (o último se bons nomes também forem escolhidos, é claro).
  • Se o sentinela for especificado como primeiro parâmetro, todos os argumentos precisam ser especificados por nome.
  • Ao chamar a função, é possível alternar para o modo posicional usando o objeto sentinela __named_only_start na posição correspondente.
  • Um desempenho melhor do que outras alternativas pode ser antecipado.

Contras:

  • A verificação ocorre durante o tempo de execução, não durante o tempo de compilação.
  • Uso de um parâmetro extra (embora não seja um argumento) e uma verificação adicional. Pequena degradação do desempenho em relação às funções regulares.
  • A funcionalidade é um hack sem suporte direto do idioma (veja a nota abaixo).
  • Ao chamar a função, é possível alternar para o modo posicional usando o objeto sentinela __named_only_startna posição correta. Sim, isso também pode ser visto como um profissional.

Lembre-se de que esta resposta só é válida para Python 2. O Python 3 implementa o mecanismo semelhante, mas muito elegante, com suporte de linguagem descrito em outras respostas.

Descobri que quando abro minha mente e penso sobre isso, nenhuma pergunta ou decisão de outra pessoa parece estúpida, idiota ou simplesmente boba. Muito pelo contrário: normalmente aprendo muito.

Mario rossi
fonte
"A verificação ocorre durante o tempo de execução, não durante a compilação." - Acho que isso é verdade para todas as verificações de argumentos de função. Até que você realmente execute a linha de invocação da função, você nem sempre sabe qual função está sendo executada. Além disso, +1 - isso é inteligente.
Eric
@Eric: É que eu preferia a verificação estática. Mas você está certo: isso não teria sido Python. Embora não seja um ponto decisivo, a construção "*" do Python 3 também é verificada dinamicamente. Obrigado por seu comentário.
Mario Rossi
Além disso, se você nomear a variável do módulo _named_only_start, torna-se impossível referenciá-la de um módulo externo, o que remove um prós e um contra. (os sublinhados principais únicos no escopo do módulo são privados, IIRC)
Eric
Em relação à nomeação do sentinela, também poderíamos ter um __named_only_starte um named_only_start(sem sublinhado inicial), o segundo para indicar que o modo nomeado é "recomendado", mas não ao nível de ser "promovido ativamente" (como um é público e o outro não é). Quanto à "privacidade" de _namescomeçar com sublinhados, ela não é muito fortemente imposta pela linguagem: pode ser facilmente contornada pelo uso de importações específicas (não *) ou nomes qualificados. É por isso que vários documentos Python preferem usar o termo "não público" em vez de "privado".
Mario Rossi
6

Você pode fazer isso de uma forma que funcione no Python 2 e no Python 3 , criando um argumento de primeira palavra-chave "falso" com um valor padrão que não ocorrerá "naturalmente". Esse argumento de palavra-chave pode ser precedido por um ou mais argumentos sem valor:

_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")

Isso permitirá:

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

mas não:

info(odbchelper, 12)                

Se você alterar a função para:

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

então, todos os argumentos devem ter palavras-chave e info(odbchelper)não funcionarão mais.

Isso permitirá que você posicione argumentos de palavra-chave adicionais em qualquer lugar depois _kw, sem forçá-lo a colocá-los após a última entrada. Isso geralmente faz sentido, por exemplo, agrupar coisas logicamente ou organizar palavras-chave em ordem alfabética pode ajudar na manutenção e no desenvolvimento.

Portanto, não há necessidade de voltar a usar def(**kwargs)e perder as informações de assinatura em seu editor inteligente. Seu contrato social é fornecer certas informações, forçando (algumas delas) a exigir palavras-chave, a ordem em que são apresentadas, tornou-se irrelevante.

Anthon
fonte
2

Atualizar:

Percebi que usar **kwargsnão resolveria o problema. Se seus programadores alteram os argumentos da função como desejam, é possível, por exemplo, alterar a função para esta:

def info(foo, **kwargs):

e o código antigo quebraria novamente (porque agora cada chamada de função tem que incluir o primeiro argumento).

Na verdade, tudo se resume ao que Bryan diz.


(...) as pessoas podem estar adicionando parâmetros entre spacinge collapse(...)

Em geral, ao alterar funções, novos argumentos devem sempre ir para o final. Caso contrário, ele quebra o código. Deve ser óbvio.
Se alguém alterar a função para que o código seja interrompido, essa alteração deve ser rejeitada.
(Como diz Bryan, é como um contrato)

(...) às vezes nem sempre fica claro o que precisa entrar.

Olhando para a assinatura da função (isto é def info(object, spacing=10, collapse=1)), deve-se ver imediatamente que todo argumento que não tem um valor padrão é obrigatório.
Para que serve o argumento, deve ir para a docstring.


Resposta antiga (mantida para integridade) :

Esta provavelmente não é uma boa solução:

Você pode definir funções desta forma:

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é um dicionário que contém qualquer argumento de palavra-chave. Você pode verificar se um argumento obrigatório está presente e, se não, gerar uma exceção.

A desvantagem é que pode não ser mais tão óbvio, quais argumentos são possíveis, mas com uma docstring adequada, deve estar tudo bem.

Felix Kling
fonte
3
Gostei mais da sua resposta antiga. Basta colocar um comentário explicando porque você está aceitando apenas ** kwargs na função. Afinal, qualquer pessoa pode mudar qualquer coisa no código-fonte - você precisa de documentação para descrever a intenção e o propósito por trás de suas decisões.
Brandon
Não há uma resposta real nesta resposta!
Phil
2

Os argumentos somente de palavra-chave do python3 ( *) podem ser simulados em python2.x com**kwargs

Considere o seguinte código python3:

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

e seu comportamento:

>>> 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'

Isso pode ser simulado usando o seguinte, observe que tomei a liberdade de alternar TypeErrorpara KeyErrorno caso do "argumento nomeado necessário", não seria muito trabalhoso tornar esse mesmo tipo de exceção também

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)

E comportamento:

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

A receita funciona igualmente bem em python3.x, mas deve ser evitada se você for apenas python3.x

Anthony Sottile
fonte
Ah, então kwargs.pop('foo')é um idioma Python 2? Preciso atualizar meu estilo de codificação. Eu ainda estava usando essa abordagem no Python 3 🤔
Neil
0

Você pode declarar suas funções como **argsapenas recebedoras . Isso exigiria argumentos de palavra-chave, mas você teria algum trabalho extra para garantir que apenas nomes válidos fossem passados.

def foo(**args):
   print args

foo(1,2) # Raises TypeError: foo() takes exactly 0 arguments (2 given)
foo(hello = 1, goodbye = 2) # Works fine.
Noufal Ibrahim
fonte
1
Você não só precisa adicionar verificações de palavras-chave, mas pensar em um consumidor que sabe que precisa chamar um método com a assinatura foo(**kwargs). O que eu passo nisso? foo(killme=True, when="rightnowplease")
Dagrooms
Depende muito. Considere dict.
Noufal Ibrahim
0

Como outras respostas dizem, alterar as assinaturas de função é uma má ideia. Adicione novos parâmetros ao final ou corrija cada chamador se forem inseridos argumentos.

Se você ainda quiser fazer isso, use um decorador de função e a função inspect.getargspec . Seria usado mais ou menos assim:

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

A implementação de require_named_argsé deixada como um exercício para o leitor.

Eu não me incomodaria. Será lento cada vez que a função for chamada, e você obterá melhores resultados escrevendo código com mais cuidado.

Daniel Newby
fonte
-1

Você pode usar o **operador:

def info(**kwargs):

desta forma, as pessoas são forçadas a usar parâmetros nomeados.

Olivier Verdier
fonte
2
E não tem ideia de como chamar seu método sem ler seu código, aumentando a carga cognitiva em seu consumidor :(
Dagrooms
Por causa do motivo mencionado, esta é uma prática muito ruim e deve ser evitada.
David S.
-1
def cheeseshop(kind, *arguments, **keywords):

em python, se usar * args, isso significa que você pode passar n-número de argumentos para este parâmetro - que virá uma lista dentro da função para acessar

e se usar ** kw significa seus argumentos de palavra-chave, que podem ser acessados ​​como dict - você pode passar n-número de argumentos kw, e se quiser restringir esse usuário deve inserir a sequência e os argumentos para não usar * e ** - (sua forma pitônica de fornecer soluções genéricas para grandes arquiteturas ...)

se você quiser restringir sua função com valores padrão, você pode verificar dentro dela

def info(object, spacing, collapse)
  spacing = spacing or 10
  collapse = collapse or 1
Shahjapan
fonte
o que acontece se o espaçamento desejado for 0? (resposta, você ganha 10). Esta resposta está tão errada quanto todas as outras respostas do ** kwargs pelos mesmos motivos.
Phil
-2

Não entendo por que um programador adicionará um parâmetro entre dois outros em primeiro lugar.

Se você deseja que os parâmetros da função sejam usados ​​com nomes (por exemplo info(spacing=15, object=odbchelper)), não deve importar em que ordem eles são definidos, então você pode também colocar os novos parâmetros no final.

Se você deseja que o pedido seja importante, não pode esperar que nada funcione se você alterá-lo!

Umang
fonte
2
Isso não responde à pergunta. Se é uma boa ideia ou não, é irrelevante - alguém pode fazer isso de qualquer maneira.
Graeme Perrow
1
Como Graeme mencionou, alguém vai fazer isso de qualquer maneira. Além disso, se você estiver escrevendo uma biblioteca para ser usada por outros, forçar (somente python 3) a passagem de argumentos apenas de palavra-chave permite flexibilidade extra quando você precisa refatorar sua API.
s0undt3ch