Come verificare se il parametro della funzione opzionale è impostato


93

Esiste un modo semplice in Python per verificare se il valore di un parametro opzionale proviene dal suo valore predefinito o perché l'utente lo ha impostato esplicitamente alla chiamata della funzione?


12
Perché voglio controllarlo in quella funzione ovviamente :)
Matthias

2
Basta usarlo Nonecome predefinito e verificarlo. Se davvero potessi impostare questo test, escluderesti anche qualsiasi possibilità per l'utente di passare esplicitamente il valore che richiama il comportamento predefinito.
Michael J. Barber,

3
Ciò può essere fatto in un modo molto più riutilizzabile e bello rispetto alla risposta che hai accettato, almeno per CPython. Vedi la mia risposta di seguito.
Ellioh

2
@Volatility: è importante se hai due serie di impostazioni predefinite. Considera una classe ricorsiva: l' Class My(): def __init__(self, _p=None, a=True, b=True, c=False) utente la chiama con x=My(b=False). Un metodo di classe potrebbe chiamare se stesso con x=My(_p=self, c=True)se le funzioni potrebbero rilevare che b non è impostato in modo esplicito e che le variabili non impostate devono essere passate dal livello superiore. Ma se non è possibile, le chiamate ricorsive devono passare tutte le variabili in modo esplicito: x=My(a=self.a, b=self.b, c=True, d=self.d, ...).
Dave

@ Dave ma è di questo che si tratta? Nella mia comprensione, la domanda è chiedere come differenziare x=My()e x=My(a=True). Il tuo scenario prevede l'assegnazione di parametri opzionali un valore diverso dal loro valore predefinito.
Volatilità

Risposte:


19

Molte risposte contengono piccole parti delle informazioni complete, quindi mi piacerebbe riunirle tutte insieme ai miei pattern preferiti.

il valore predefinito è un mutabletipo

Se il valore predefinito è un oggetto modificabile, sei fortunato: puoi sfruttare il fatto che gli argomenti predefiniti di Python vengono valutati una volta quando la funzione è definita (un po 'di più su questo alla fine della risposta nell'ultima sezione)

Ciò significa che puoi facilmente confrontare un valore modificabile predefinito utilizzando isper vedere se è stato passato come argomento o lasciato per impostazione predefinita, come negli esempi seguenti come funzione o metodo:

def f(value={}):
    if value is f.__defaults__[0]:
        print('default')
    else:
        print('passed in the call')

e

class A:
    def f(self, value={}):
        if value is self.f.__defaults__[0]:
            print('default')
        else:
            print('passed in the call')

Argomenti predefiniti immutabili

Ora, è un po 'meno elegante se il tuo valore predefinito dovrebbe essere un immutablevalore (e ricorda che anche le stringhe sono immutabili!) Perché non puoi sfruttare il trucco così com'è, ma c'è ancora qualcosa che puoi fare, sfruttando ancora mutabile genere; fondamentalmente si inserisce un valore predefinito "falso" mutabile nella firma della funzione e il valore predefinito "reale" desiderato nel corpo della funzione.

def f(value={}):
    """
    my function
    :param value: value for my function; default is 1
    """
    if value is f.__defaults__[0]:
        print('default')
        value = 1
    else:
        print('passed in the call')
    # whatever I want to do with the value
    print(value)

È particolarmente divertente se il valore predefinito reale è None, ma Noneè immutabile, quindi ... è comunque necessario utilizzare esplicitamente un mutabile come parametro predefinito della funzione e passare a Nessuno nel codice.

Utilizzo di una Defaultclasse per valori predefiniti immutabili

o, in modo simile al suggerimento di @cz, se i documenti di Python non sono sufficienti :-), puoi aggiungere un oggetto in mezzo per rendere l'API più esplicita (senza leggere i documenti); l'istanza della classe used_proxy_ Default è modificabile e conterrà il valore predefinito reale che si desidera utilizzare.

class Default:
    def __repr__(self):
        return "Default Value: {} ({})".format(self.value, type(self.value))

    def __init__(self, value):
        self.value = value

def f(default=Default(1)):
    if default is f.__defaults__[0]:
        print('default')
        print(default)
        default = default.value
    else:
        print('passed in the call')
    print("argument is: {}".format(default))

adesso:

>>> f()
default
Default Value: 1 (<class 'int'>)
argument is: 1

>>> f(2)
passed in the call
argument is: 2

Quanto sopra funziona bene anche per Default(None).

Altri modelli

Ovviamente i modelli di cui sopra sembrano più brutti di quanto dovrebbero a causa di tutti quelli printche sono lì solo per mostrare come funzionano. Altrimenti li trovo abbastanza concisi e ripetibili.

Potresti scrivere un decoratore per aggiungere il __call__pattern suggerito da @dmg in un modo più semplificato, ma questo obbligherà comunque a usare strani trucchi nella definizione della funzione stessa: dovresti dividerlo valuee value_defaultse il tuo codice ha bisogno di distinguerli, quindi Non vedo molti vantaggi e non scriverò l'esempio :-)

Tipi mutabili come valori predefiniti in Python

Qualcosa in più sul # 1 gotcha di Python! , abusato per il tuo piacere sopra. Puoi vedere cosa succede a causa della valutazione alla definizione facendo:

def testme(default=[]):
    print(id(default))

Puoi eseguire testme()tutte le volte che vuoi, vedrai sempre un riferimento alla stessa istanza predefinita (quindi in pratica il tuo valore predefinito è immutabile :-)).

Ricordate che in Python ci sono solo 3 mutabili tipi built-in : set, list, dict; tutto il resto - anche gli archi! - è immutabile.


L'esempio che hai in "Argomenti predefiniti immutabili" non ha effettivamente un argomento predefinito immutabile. Se lo facesse, non funzionerebbe.
Karol

@ Karol, ti va di elaborare? Il valore predefinito in questo esempio è 1, che dovrebbe essere immutabile ...
Stefano

Vedo la firma della funzione come def f(value={}).
Karol

1
Ah, ora ho capito, grazie. Non è facile da seguire a meno che qualcuno non legga il tuo testo molto attentamente, cosa che potrebbe non accadere spesso su SO. Considera la possibilità di riformulare.
Karol

1
In "if default è f .__ defaults __ [0]:", è necessario codificare il numero di parametro predefinito da utilizzare, che potrebbe essere fragile se la firma della funzione cambia. Un'alternativa è "if default in f .__ defaults__:". Supponendo che tu utilizzi un'istanza predefinita diversa per ogni argomento, "in" dovrebbe funzionare altrettanto bene di "è".
Stephen Warren

57

Non proprio. Il modo standard consiste nell'usare un valore predefinito che l'utente non dovrebbe passare, ad objectesempio un'istanza:

DEFAULT = object()
def foo(param=DEFAULT):
    if param is DEFAULT:
        ...

Di solito puoi semplicemente usare Nonecome valore predefinito, se non ha senso come valore che l'utente vorrebbe passare.

L'alternativa è usare kwargs:

def foo(**kwargs):
    if 'param' in kwargs:
        param = kwargs['param']
    else:
        ...

Tuttavia, questo è eccessivamente dettagliato e rende la funzione più difficile da usare poiché la sua documentazione non includerà automaticamente il paramparametro.


9
Ho anche visto diverse persone utilizzare il builtin Ellipsis per i luoghi in cui è necessario e Nessuno è considerato un input valido. Questo è essenzialmente lo stesso del primo esempio.
GrandOpener

Se si desidera implementare un comportamento speciale se è stato passato Nessuno, ma è comunque necessario un modo per verificare se l'argomento è stato fornito dall'utente, è possibile utilizzare il Ellipsissingleton come predefinito, che è stato esplicitamente progettato per essere utilizzato come ignora questo valore. ...è un alias per Ellipsis, quindi gli utenti che desiderano utilizzare argomenti posizionali possono semplicemente chiamare, il your_function(p1, ..., p3)che lo rende ovvio e piacevole da leggere.
Bachsau

However this is overly verbose and makes your function more difficult to use as its documentation will not automatically include the param parameter.Questo in realtà non è vero, poiché è possibile impostare la descrizione di una funzione e dei suoi parametri utilizzando il inspectmodulo. Dipende dal tuo IDE se funzionerà o meno.
EZLearner

15

Il seguente decoratore di funzioni,, explicit_checkercrea una serie di nomi di parametri di tutti i parametri forniti esplicitamente. Aggiunge il risultato come parametro extra ( explicit_params) alla funzione. Basta fare 'a' in explicit_paramsper verificare se il parametro aè dato esplicitamente.

def explicit_checker(f):
    varnames = f.func_code.co_varnames
    def wrapper(*a, **kw):
        kw['explicit_params'] = set(list(varnames[:len(a)]) + kw.keys())
        return f(*a, **kw)
    return wrapper

@explicit_checker
def my_function(a, b=0, c=1, explicit_params=None):
    print a, b, c, explicit_params
    if 'b' in explicit_params:
        pass # Do whatever you want


my_function(1)
my_function(1, 0)
my_function(1, c=1)

Questo codice funziona solo in python2. Per python 3, vedi la mia risposta di seguito: stackoverflow.com/questions/14749328/…
R. Yang

1
Questo è abbastanza interessante, ma è meglio evitare il problema con un design migliore in primo luogo, se possibile.
Karol

@ Karol, sono d'accordo. Nella maggior parte dei casi non dovrebbe essere necessario se il design è ragionevole.
Ellioh

4

A volte uso una stringa universalmente unica (come un UUID).

import uuid
DEFAULT = uuid.uuid4()
def foo(arg=DEFAULT):
  if arg is DEFAULT:
    # it was not passed in
  else:
    # it was passed in

In questo modo, nessun utente potrebbe nemmeno indovinare il valore predefinito se ha provato, quindi posso essere molto sicuro che quando vedo quel valore per arg, non è stato passato.


4
Gli oggetti Python sono riferimenti, puoi semplicemente usare al object()posto di uuid4()- è ancora un'istanza unica , che è ciò che iscontrolla
cz

3

Ho visto questo modello un paio di volte (ad esempio biblioteca unittest, py-flags, jinja):

class Default:
    def __repr__( self ):
        return "DEFAULT"

DEFAULT = Default()

... o l'equivalente one-liner ...:

DEFAULT = type( 'Default', (), { '__repr__': lambda x: 'DEFAULT' } )()

Diversamente DEFAULT = object(), questo aiuta il controllo del tipo e fornisce informazioni quando si verificano errori - spesso nei messaggi di errore vengono utilizzate la rappresentazione di stringa ( "DEFAULT") o il nome della classe ( "Default").


3

La risposta di @ Ellioh funziona in python 2. In python 3, il seguente codice dovrebbe funzionare:

import inspect
def explicit_checker(f):
    varnames = inspect.getfullargspec(f)[0]
    def wrapper(*a, **kw):
        kw['explicit_params'] = set(list(varnames[:len(a)]) + list(kw.keys()))
        return f(*a, **kw)
    return wrapper

@explicit_checker
def my_function(a, b=0, c=1, explicit_params=None):
    print a, b, c, explicit_params
    if 'b' in explicit_params:
        pass # Do whatever you want

Questo metodo può mantenere i nomi degli argomenti ei valori predefiniti (invece di ** kwargs) con una migliore leggibilità.


3

Puoi verificarlo da foo.__defaults__efoo.__kwdefaults__

vedere un semplice esempio qui sotto

def foo(a, b, c=123, d=456, *, e=789, f=100):
    print(foo.__defaults__)
    # (123, 456) 
    print(foo.__kwdefaults__)
    # {'e': 789, 'f': 100}
    print(a, b, c, d, e, f)

#and these variables are also accessible out of function body
print(foo.__defaults__)    
# (123, 456)  
print(foo.__kwdefaults__)  
# {'e': 789, 'f': 100}

foo.__kwdefaults__['e'] = 100500

foo(1, 2) 
#(123, 456)
#{'f': 100, 'e': 100500}
#1 2 123 456 100500 100

quindi usando l'operatore =e ispuoi confrontarli

e per alcuni casi il codice qui sotto è sufficiente

Ad esempio, è necessario evitare di modificare il valore predefinito, quindi è possibile verificare l'uguaglianza e quindi copiare se è così

    def update_and_show(data=Example):
        if data is Example:
            data = copy.deepcopy(data)
        update_inplace(data) #some operation
        print(data)

Inoltre, è abbastanza comodo da usare getcallargsda inspectcome si ritorna argomenti reali con cui verrà richiamato la funzione. Se passi una funzione e args e kwargs ( inspect.getcallargs(func, /, *args, **kwds)), restituirà gli argomenti del metodo reale usati per l'invocazione, prendendo in considerazione i valori predefiniti e altre cose. Dai un'occhiata a un esempio qui sotto.

from inspect import getcallargs

# we have a function with such signature
def show_params(first, second, third=3):
    pass

# if you wanted to invoke it with such params (you could get them from a decorator as example)
args = [1, 2, 5]
kwargs = {}
print(getcallargs(show_params, *args, **kwargs))
#{'first': 1, 'second': 2, 'third': 5}

# here we didn't specify value for d
args = [1, 2, 3, 4]
kwargs = {}

# ----------------------------------------------------------
# but d has default value =7
def show_params1(first, *second, d = 7):
    pass


print(getcallargs(show_params1, *args, **kwargs))
# it will consider b to be equal to default value 7 as it is in real method invocation
# {'first': 1, 'second': (2, 3, 4), 'd': 7}

# ----------------------------------------------------------
args = [1]
kwargs = {"d": 4}

def show_params2(first, d=3):
    pass


print(getcallargs(show_params2, *args, **kwargs))
#{'first': 1, 'd': 4}

https://docs.python.org/3/library/inspect.html


2

Sono d'accordo con il commento di Volatility. Ma puoi controllare nel modo seguente:

def function(arg1,...,**optional):
    if 'optional_arg' in optional:
        # user has set 'optional_arg'
    else:
        # user has not set 'optional_arg'
        optional['optional_arg'] = optional_arg_default_value # set default

Credo che un parametro opzionale sia qualcosa come def func(optional=value)no**kwargs
Zaur Nasibov

È qualcosa che è in qualche modo aperto all'interpretazione. Qual è la differenza effettiva tra un argomento con un valore predefinito e un argomento parola chiave? Entrambi sono espressi utilizzando la stessa sintassi "parola chiave = valore".
isedev

Non sono d'accordo, perché lo scopo dei parametri opzionali ed **kwargsè un po 'diverso. PS nessun problema riguarda -1 :) E il mio -1 per te è stato accidentale :)
Zaur Nasibov

2

Questa è una variazione della risposta di stefano, ma la trovo un po 'più leggibile:

not_specified = {}

def foo(x=not_specified):
    if x is not_specified:
            print("not specified")
    else:
            print("specified")

Un voto positivo ?? Mi piace di più. Semplice, nessun riflesso. Leggibile.
Vincent,

1

Un approccio un po 'bizzarro sarebbe:

class CheckerFunction(object):
    def __init__(self, function, **defaults):
        self.function = function
        self.defaults = defaults

    def __call__(self, **kwargs):
        for key in self.defaults:
            if(key in kwargs):
                if(kwargs[key] == self.defaults[key]):
                    print 'passed default'
                else:
                    print 'passed different'
            else:
                print 'not passed'
                kwargs[key] = self.defaults[key]

        return self.function(**kwargs)

def f(a):
    print a

check_f = CheckerFunction(f, a='z')
check_f(a='z')
check_f(a='b')
check_f()

Quali uscite:

passed default
z
passed different
b
not passed
z

Ora questo, come ho detto, è piuttosto strano, ma fa il lavoro. Tuttavia questo è abbastanza illeggibile e in modo simile a ecatmur 's suggerimento non sarà documentato automaticamente.


1
Potresti includere il comportamento di check_f('z'), che è anche, come dici tu, bizzarro.
Michael J. Barber,

@ MichaelJ.Barber Buon punto. Dovrai fare un po 'di "magia" anche con * args. Tuttavia, il mio punto era che è possibile, ma ora è necessario che il valore predefinito venga passato o meno è una cattiva progettazione.
dmg
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.