web-dev-qa-db-fra.com

Python: comment vérifier si le paramètre de fonction optionnel est défini

Existe-t-il un moyen simple de vérifier si la valeur d'un paramètre facultatif provient de sa valeur par défaut ou si l'utilisateur l'a définie explicitement à l'appel de la fonction?

49
Matthias

Pas vraiment. La méthode standard consiste à utiliser une valeur par défaut que l'utilisateur ne devrait pas transmettre, par exemple. une instance object:

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

Habituellement, vous pouvez simplement utiliser None comme valeur par défaut, si cela n’a pas de sens en tant que valeur que l’utilisateur souhaite transmettre.

L'alternative consiste à utiliser kwargs:

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

Cependant, ceci est trop détaillé et rend votre fonction plus difficile à utiliser car sa documentation n'inclura pas automatiquement le paramètre param.

38
ecatmur

Le décorateur de fonction suivant, explicit_checker, crée un ensemble de noms de paramètres de tous les paramètres donnés explicitement. Il ajoute le résultat en tant que paramètre supplémentaire (explicit_params) à la fonction. Il suffit de faire 'a' in explicit_params pour vérifier si le paramètre a est donné explicitement.

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

J'utilise parfois une chaîne universellement unique (comme 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

De cette façon, aucun utilisateur ne pourrait même deviner la valeur par défaut s'il essayait. Je peux donc être sûr que lorsque je vois cette valeur pour arg, elle n'a pas été transmise.

5
Jesse B Miller

Je suis d'accord avec le commentaire de la volatilité. Mais vous pouvez vérifier de la manière suivante:

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

Vous pouvez le vérifier à partir de foo.__defaults__ et foo.__kwdefaults__

voir un exemple simple ci-dessous

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

puis en utilisant l'opérateur = et is vous pouvez les comparer

et dans certains cas, le code ci-dessous est suffisant

Par exemple, vous devez éviter de modifier la valeur par défaut, vous pouvez alors vérifier l’égalité, puis vous en sortir

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

Beaucoup de réponses contiennent de petits morceaux d'informations complètes, alors j'aimerais les rassembler avec mes motifs préférés.

la valeur par défaut est un type mutable

Si la valeur par défaut est un objet mutable, vous avez de la chance: vous pouvez exploiter le fait que les arguments par défaut de Python sont évalués une fois lorsque la fonction est définie (pour en savoir plus à la fin de la réponse)

Cela signifie que vous pouvez facilement comparer une valeur modifiable par défaut en utilisant is pour voir si elle a été transmise en tant qu'argument ou laissée par défaut, comme dans les exemples suivants en tant que fonction ou méthode:

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

et

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

Arguments par défaut immuables

Maintenant, il est un peu moins élégant si votre valeur par défaut est supposée être une valeur immutable (et rappelez-vous que même les chaînes sont immuables!) Car vous ne pouvez pas exploiter le truc tel quel, mais vous pouvez toujours faire quelque chose comme: :

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)

Cela semble particulièrement amusant si votre valeur par défaut est None, mais None est immuable alors ...

Utilisation d'une classe Default pour des valeurs par défaut immuables

ou, comme dans la suggestion de @ c-z, si python docs ne suffisent pas :-), vous pouvez ajouter un objet entre eux pour rendre l'API plus explicite sans lire les documents:

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

à présent:

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

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

Ce qui précède fonctionne bien aussi pour Default(None).

D'autres modèles

Évidemment, les schémas ci-dessus semblent plus laids qu’ils ne le devraient à cause de tous les print qui ne sont là que pour montrer comment ils fonctionnent. Sinon, je les trouve assez concis et répétables.

Vous pouvez écrire à un décorateur pour ajouter le motif __call__ suggéré par @dmg d'une manière plus simple, mais cela obligera tout de même à utiliser des astuces étranges dans la définition de la fonction elle-même - il vous faudra alors séparer value et value_default si votre code doit les distinguer, je ne vois donc pas beaucoup d'avantages et je n'écrirai pas l'exemple :-)

Types mutables comme valeurs par défaut

Un peu plus sur # 1 python gotcha! , abusé pour votre propre plaisir ci-dessus. Vous pouvez voir ce qui se passe en raison de l'évaluation à la définition en faisant:

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

Vous pouvez exécuter testme() autant de fois que vous le souhaitez, vous verrez toujours une référence à la même instance par défaut (votre commande par défaut est donc immuable :-)).

Rappelez-vous que dans Python, il y a 3 mutables types intégrés : set, list, dict, mais tout le reste - même des chaînes ! - est immuable.

1
Stefano

J'ai vu ce modèle à quelques reprises (par exemple, bibliothèque unittest, py-flags, jinja):

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

DEFAULT = Default()

... ou l'équivalent one-liner ...:

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

Contrairement à DEFAULT = object(), cela facilite la vérification de type et fournit des informations lorsque des erreurs se produisent - la représentation sous forme de chaîne ("DEFAULT") ou le nom de classe ("Default") sont fréquemment utilisés dans les messages d'erreur.

0
c z

Une approche un peu bizarre serait:

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

Quelles sorties:

passed default
z
passed different
b
not passed
z

Maintenant, comme je l'ai dit, c'est assez bizarre, mais ça fait le travail. Cependant, ceci est assez illisible et similaire à la suggestion de ecatmur / ne sera pas automatiquement documentée.

0
dmg