Denominazione forzata dei parametri in Python


111

In Python potresti avere una definizione di funzione:

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

che potrebbe essere chiamato in uno dei seguenti modi:

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

grazie a Python che consente argomenti di qualsiasi ordine, purché siano nominati.

Il problema che stiamo riscontrando è che man mano che alcune delle nostre funzioni più grandi crescono, le persone potrebbero aggiungere parametri tra spacinge collapse, il che significa che i valori sbagliati potrebbero andare a parametri che non sono nominati. Inoltre, a volte non è sempre chiaro ciò che deve essere inserito. Stiamo cercando un modo per costringere le persone a nominare determinati parametri - non solo uno standard di codifica, ma idealmente un flag o un plugin pydev?

in modo che nei 4 esempi precedenti, solo l'ultimo passerebbe il controllo poiché tutti i parametri sono denominati.

È probabile che lo accenderemo solo per determinate funzioni, ma qualsiasi suggerimento su come implementarlo o se fosse possibile sarebbe apprezzato.

Risposte:


214

In Python 3 - Sì, puoi specificare *nell'elenco degli argomenti.

Dai documenti :

I parametri dopo "*" o "* identificatore" sono parametri di sola parola chiave e possono essere passati solo argomenti di parole chiave utilizzate.

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

Questo può anche essere combinato con **kwargs:

def foo(pos, *, forcenamed, **kwargs):

32

Puoi costringere le persone a utilizzare argomenti di parole chiave in Python3 definendo una funzione nel modo seguente.

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

Rendendo il primo argomento un argomento posizionale senza nome costringi tutti coloro che chiamano la funzione a utilizzare gli argomenti della parola chiave, che è quello che penso tu stia chiedendo. In Python2 l'unico modo per farlo è definire una funzione come questa

def foo(**kwargs):
    pass

Ciò costringerà il chiamante a usare kwargs ma questa non è una soluzione eccezionale in quanto dovresti quindi mettere un segno di spunta per accettare solo l'argomento di cui hai bisogno.


11

È vero, la maggior parte dei linguaggi di programmazione rende l'ordine dei parametri parte del contratto di chiamata di funzione, ma non è necessario che sia così. Perché dovrebbe? La mia comprensione della domanda è, quindi, se Python è diverso da altri linguaggi di programmazione in questo senso. Oltre ad altre buone risposte per Python 2, considera quanto segue:

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

L'unico modo in cui un chiamante sarebbe in grado di fornire argomenti spacinge collapseposizionalmente (senza eccezioni) sarebbe:

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

La convenzione di non utilizzare elementi privati ​​appartenenti ad altri moduli è già molto semplice in Python. Come con Python stesso, questa convenzione per i parametri sarebbe solo parzialmente applicata.

In caso contrario, le chiamate dovrebbero essere nel formato:

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

Una chiamata

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

assegnerebbe il valore 11 al parametro _pe un'eccezione sollevata dalla prima istruzione della funzione.

caratteristiche:

  • I parametri precedenti _p=__named_only_startsono ammessi in posizione (o per nome).
  • I parametri dopo _p=__named_only_startdevono essere forniti solo per nome (a meno che non __named_only_startsi ottenga e si utilizzi la conoscenza dell'oggetto speciale sentinella ).

Professionisti:

  • I parametri sono espliciti in numero e significato (il secondo se vengono scelti anche nomi buoni, ovviamente).
  • Se la sentinella è specificata come primo parametro, tutti gli argomenti devono essere specificati per nome.
  • Quando si chiama la funzione, è possibile passare alla modalità posizionale utilizzando l'oggetto sentinel __named_only_startnella posizione corrispondente.
  • È possibile prevedere prestazioni migliori rispetto ad altre alternative.

Contro:

  • Il controllo si verifica durante l'esecuzione, non durante la compilazione.
  • Utilizzo di un parametro aggiuntivo (sebbene non di un argomento) e di un controllo aggiuntivo. Piccolo degrado delle prestazioni rispetto alle normali funzioni.
  • La funzionalità è un hack senza supporto diretto da parte della lingua (vedi nota sotto).
  • Quando si chiama la funzione, è possibile passare alla modalità posizionale utilizzando l'oggetto sentinel __named_only_startnella posizione corretta. Sì, questo può anche essere visto come un professionista.

Tieni presente che questa risposta è valida solo per Python 2. Python 3 implementa il meccanismo simile, ma molto elegante, supportato dal linguaggio descritto in altre risposte.

Ho scoperto che quando apro la mente e ci penso, nessuna domanda o decisione di altri sembra stupida, stupida o semplicemente sciocca. Al contrario: di solito imparo molto.


"Il controllo si verifica durante la fase di esecuzione, non durante la compilazione." - Penso che sia vero per tutti i controlli degli argomenti delle funzioni. Finché non esegui effettivamente la riga di chiamata della funzione, non sai sempre quale funzione viene eseguita. Inoltre, +1 : questo è intelligente.
Eric

@ Eric: è solo che avrei preferito il controllo statico. Ma hai ragione: quello non sarebbe stato affatto Python. Sebbene non sia un punto decisivo, il costrutto "*" di Python 3 è anche controllato dinamicamente. Grazie per il tuo commento.
Mario Rossi

Inoltre, se si nomina la variabile del modulo _named_only_start, diventa impossibile fare riferimento ad essa da un modulo esterno, che elimina un pro e un contro. (i singoli trattini bassi iniziali nell'ambito del modulo sono privati, IIRC)
Eric

Per quanto riguarda la denominazione della sentinella, potremmo anche avere sia una __named_only_startche una named_only_start(senza trattino basso iniziale), la seconda per indicare che la modalità denominata è "consigliata", ma non al livello di essere "attivamente promossa" (poiché una è pubblica e la altro non lo è). Per quanto riguarda la "riservatezza" di _namesiniziare con i trattini bassi, non è molto fortemente applicata dalla lingua: può essere facilmente aggirata mediante l'uso di importazioni specifiche (non *) o nomi qualificati. Questo è il motivo per cui molti documenti Python preferiscono usare il termine "non pubblico" invece di "privato".
Mario Rossi

6

Puoi farlo in un modo che funziona sia in Python 2 che in Python 3 , creando un primo argomento di parola chiave "fasullo" con un valore predefinito che non si verificherà "naturalmente". L'argomento della parola chiave può essere preceduto da uno o più argomenti senza valore:

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

Ciò consentirà:

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

ma no:

info(odbchelper, 12)                

Se modifichi la funzione in:

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

quindi tutti gli argomenti devono avere parole chiave e info(odbchelper)non funzioneranno più.

Ciò ti consentirà di posizionare ulteriori argomenti di parole chiave in qualsiasi punto successivo _kw, senza costringerti a metterli dopo l'ultima voce. Questo spesso ha senso, ad esempio raggruppare le cose in modo logico o disporre le parole chiave in ordine alfabetico può aiutare con la manutenzione e lo sviluppo.

Quindi non è necessario tornare a utilizzare def(**kwargs)e perdere le informazioni sulla firma nel tuo editor intelligente. Il tuo contratto sociale consiste nel fornire determinate informazioni, costringendo (alcune di esse) a richiedere parole chiave, l'ordine in cui sono presentate è diventato irrilevante.


2

Aggiornare:

Mi sono reso conto che l'utilizzo **kwargsnon avrebbe risolto il problema. Se i tuoi programmatori cambiano gli argomenti della funzione come desiderano, si potrebbe, ad esempio, cambiare la funzione in questo:

def info(foo, **kwargs):

e il vecchio codice si romperebbe di nuovo (perché ora ogni chiamata di funzione deve includere il primo argomento).

Dipende davvero da ciò che dice Bryan.


(...) le persone potrebbero aggiungere parametri tra spacinge collapse(...)

In generale, quando si cambiano le funzioni, i nuovi argomenti dovrebbero sempre andare alla fine. Altrimenti rompe il codice. Dovrebbe essere ovvio.
Se qualcuno cambia la funzione in modo che il codice si interrompa, questa modifica deve essere rifiutata.
(Come dice Bryan, è come un contratto)

(...) a volte non è sempre chiaro cosa debba essere inserito.

Osservando la firma della funzione (cioè def info(object, spacing=10, collapse=1)) si dovrebbe immediatamente vedere che ogni argomento che non ha un valore predefinito, è obbligatorio.
A cosa serve l'argomento, dovrebbe andare nella docstring.


Vecchia risposta (mantenuta per completezza) :

Questa probabilmente non è una buona soluzione:

Puoi definire le funzioni in questo modo:

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è un dizionario che contiene qualsiasi argomento di parola chiave. È possibile verificare se è presente un argomento obbligatorio e, in caso contrario, sollevare un'eccezione.

Lo svantaggio è che potrebbe non essere più così ovvio, quali argomenti sono possibili, ma con una docstring adeguata, dovrebbe andare bene.


3
Mi è piaciuta di più la tua vecchia risposta. Metti solo un commento sul motivo per cui accetti solo ** kwargs nella funzione. Dopotutto, chiunque può modificare qualsiasi cosa nel codice sorgente: è necessaria la documentazione per descrivere l'intento e lo scopo dietro le proprie decisioni.
Brandon

Non c'è una risposta reale in questa risposta!
Phil

2

Gli argomenti di sola parola chiave python3 ( *) possono essere simulati in python2.x con**kwargs

Considera il seguente codice python3:

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

e il suo 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'

Questo può essere simulato usando quanto segue, nota che mi sono preso la libertà di passare TypeErroral KeyErrorcaso "argomento con nome richiesto", non sarebbe troppo faticoso rendere anche quello lo stesso tipo di eccezione

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

La ricetta funziona allo stesso modo anche in python3.x, ma dovrebbe essere evitata se sei solo python3.x.


Ah, quindi kwargs.pop('foo')è un idioma di Python 2? Devo aggiornare il mio stile di codifica. Stavo ancora usando questo approccio in Python 3 🤔
Neil

0

Potresti dichiarare le tue funzioni come **argssolo riceventi . Ciò imporrebbe argomenti di parole chiave, ma avresti del lavoro extra per assicurarti che vengano passati solo nomi validi.

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
Non solo devi aggiungere controlli di parole chiave, ma pensa a un consumatore che sa di dover chiamare un metodo con la firma foo(**kwargs). Cosa ci passo dentro? foo(killme=True, when="rightnowplease")
Dagrooms

Dipende davvero. Considera dict.
Noufal Ibrahim

0

Come dicono altre risposte, cambiare le firme delle funzioni è una cattiva idea. Aggiungi nuovi parametri alla fine o correggi ogni chiamante se vengono inseriti argomenti.

Se vuoi ancora farlo, usa un decoratore di funzioni e la funzione inspect.getargspec . Sarebbe usato qualcosa del genere:

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

Implementazione di require_named_args è lasciata come esercizio per il lettore.

Non mi preoccuperei. Sarà lento ogni volta che viene chiamata la funzione e otterrai risultati migliori scrivendo il codice con maggiore attenzione.


-1

Potresti usare l' **operatore:

def info(**kwargs):

in questo modo le persone sono costrette a utilizzare parametri denominati.


2
E non hai idea di come chiamare il tuo metodo senza leggere il tuo codice, aumentando il carico cognitivo sul tuo consumatore :(
Dagrooms

A causa del motivo menzionato, questa è davvero una cattiva pratica e dovrebbe essere evitata.
David S.

-1
def cheeseshop(kind, *arguments, **keywords):

in python se usi * args significa che puoi passare n-numero di argomenti per questo parametro - che sarà una lista all'interno della funzione per accedere

e se usi ** kw che significa i suoi argomenti della parola chiave, puoi accedervi come dict - puoi passare un numero n di argomenti kw e se vuoi limitare quell'utente deve inserire la sequenza e gli argomenti in ordine quindi non usare * e ** - (il suo modo pitonico di fornire soluzioni generiche per grandi architetture ...)

se vuoi limitare la tua funzione con i valori predefiniti, puoi controllare al suo interno

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

cosa succede se si desidera che la spaziatura sia 0? (risposta, ottieni 10). Questa risposta è sbagliata come tutte le altre risposte ** kwarg per gli stessi motivi.
Phil

-2

Non capisco perché un programmatore aggiungerà un parametro tra altri due in primo luogo.

Se si desidera utilizzare i parametri della funzione con i nomi (es info(spacing=15, object=odbchelper) ), allora non dovrebbe importare in quale ordine sono definiti, quindi potresti anche mettere i nuovi parametri alla fine.

Se vuoi che l'ordine sia importante, non puoi aspettarti che qualcosa funzioni se lo cambi!


2
Questo non risponde alla domanda. Che sia o meno una buona idea è irrilevante: qualcuno potrebbe farlo comunque.
Graeme Perrow

1
Come ha detto Graeme, qualcuno lo farà comunque. Inoltre, se stai scrivendo una libreria per essere utilizzata da altri, forzare (solo python 3) il passaggio di argomenti di sola parola chiave consente una maggiore flessibilità quando devi rifattorizzare la tua API.
s0undt3ch
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.