Comment vérifier si le paramètre de fonction facultatif est défini

91

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

Matthias
la source
12
Parce que je veux le vérifier dans cette fonction bien sûr :)
Matthias
2
Utilisez simplement Nonecomme valeur par défaut et vérifiez cela. Si vous pouviez vraiment configurer ce test, vous excluriez également toute possibilité pour l'utilisateur de transmettre explicitement la valeur qui invoque le comportement par défaut.
Michael
3
Cela peut être fait d'une manière beaucoup plus réutilisable et belle que dans la réponse que vous avez acceptée, du moins pour CPython. Voir ma réponse ci-dessous.
Ellioh
2
@Volatility: il est important que vous ayez deux jeux de valeurs par défaut. Considérez une classe récursive: l' Class My(): def __init__(self, _p=None, a=True, b=True, c=False) utilisateur l'appelle avec x=My(b=False). Une méthode de classe pourrait s'appeler avec des x=My(_p=self, c=True)fonctions if pourraient détecter que b n'est pas explicitement défini et que les variables non définies doivent être transmises depuis le niveau supérieur. Mais si elles ne peuvent pas, les appels récursifs doivent passer toutes les variables explicitement: x=My(a=self.a, b=self.b, c=True, d=self.d, ...).
Dave
@Dave mais est-ce là la question? À ma connaissance, la question est de savoir comment différencier x=My()et x=My(a=True). Votre scénario consiste à attribuer aux paramètres facultatifs une valeur autre que leur valeur par défaut.
Volatilité

Réponses:

16

Beaucoup de réponses contiennent de petites informations complètes, je voudrais donc les rassembler avec mes motifs préférés.

la valeur par défaut est un mutabletype

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 (plus d'informations à ce sujet à la fin de la réponse dans la dernière section)

Cela signifie que vous pouvez facilement comparer une valeur mutable par défaut en utilisant ispour voir si elle a été passée en 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, c'est un peu moins élégant si on s'attend à ce que votre valeur par défaut soit une immutablevaleur (et rappelez-vous que même les chaînes sont immuables!) Parce que vous ne pouvez pas exploiter l'astuce telle quelle, mais il y a toujours quelque chose que vous pouvez faire, en exploitant toujours mutable type; en gros, vous mettez un "faux" par défaut mutable dans la signature de la fonction, et la valeur par défaut "réelle" souhaitée dans le corps de la fonction.

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 qu'elle Noneest immuable, donc ... vous devez toujours utiliser explicitement un mutable comme paramètre par défaut de la fonction et basculer sur None dans le code.

Utilisation d'une Defaultclasse pour les valeurs par défaut immuables

ou, similaire à la suggestion @cz, si les documents python ne suffisent pas :-), vous pouvez ajouter un objet entre les deux pour rendre l'API plus explicite (sans lire les documents); l'instance de classe used_proxy_ Default est mutable et contiendra la valeur par défaut réelle que vous souhaitez utiliser.

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

maintenant:

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

Autres modèles

De toute évidence, les modèles ci-dessus semblent plus laids qu'ils ne le devraient à cause de tous ceux printqui ne sont là que pour montrer comment ils fonctionnent. Sinon, je les trouve assez laconiques et répétables.

Vous pouvez écrire un décorateur pour ajouter le __call__modèle suggéré par @dmg de manière plus simple, mais cela obligera toujours à utiliser des astuces étranges dans la définition de la fonction elle-même - vous devrez vous séparer valueet value_defaultsi votre code a besoin de les distinguer, alors Je ne vois pas beaucoup d'avantage et je n'écrirai pas l'exemple :-)

Types mutables comme valeurs par défaut dans Python

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 (donc fondamentalement votre valeur par défaut est immuable :-)).

Rappelez - vous qu'en Python il y a seulement 3 mutable types intégrés : set, list, dict; tout le reste - même les cordes! - est immuable.

Stefano
la source
L'exemple que vous avez dans "Arguments par défaut immuables" n'a pas en fait d'argument par défaut immuable. Si c'était le cas, cela ne fonctionnerait pas.
Karol
@Karol, voulez-vous élaborer? La valeur par défaut dans cet exemple est 1, qui devrait être immuable ...
Stefano
Je vois la signature de la fonction comme def f(value={}).
Karol
@Karol c'est la signature, mais la valeur par défaut souhaitée est 1; désolé si ce n'est pas clair dans l'explication, mais le but de cette partie de la réponse est de pouvoir avoir une valeur par défaut immuable ( 1). Si vous vérifiez l'exemple, vous verrez qu'il dit:, print('default'); value = 1nonvalue={}
Stefano
1
Ha, je comprends maintenant, merci. Ce n'est pas facile à suivre à moins que quelqu'un ne lise votre texte très attentivement, ce qui pourrait ne pas arriver si souvent sur SO. Pensez à reformuler.
Karol
56

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

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

Habituellement, vous pouvez simplement utiliser Nonecomme valeur par défaut, si cela n'a pas de sens en tant que valeur que l'utilisateur voudrait transmettre.

L'alternative est d'utiliser kwargs:

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

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

ecatmur
la source
8
J'ai également vu plusieurs personnes utiliser la fonction intégrée Ellipsis pour les endroits où cela est nécessaire et None est considéré comme une entrée valide. C'est essentiellement le même que le premier exemple.
GrandOpener
Si vous souhaitez implémenter un comportement spécial si None a été passé, mais que vous avez toujours besoin d'un moyen de tester si l'argument a été donné par l'utilisateur, vous pouvez utiliser le Ellipsissingleton par défaut, qui a été explicitement conçu pour être utilisé comme saut de cette valeur. ...est un alias pour Ellipsis, donc les utilisateurs qui souhaitent utiliser des arguments de position peuvent simplement appeler your_function(p1, ..., p3)ce qui le rend évident et agréable à lire.
Bachsau
However this is overly verbose and makes your function more difficult to use as its documentation will not automatically include the param parameter.C'est en fait faux, car vous pouvez définir la description d'une fonction et de ses paramètres à l'aide du inspectmodule. Cela dépend de votre IDE si cela fonctionnera ou non.
EZLearner
15

Le décorateur de fonction suivant,, explicit_checkercré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. Faites juste 'a' in explicit_paramspour vérifier si le paramètre aest 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)
Ellioh
la source
Ce code fonctionne uniquement en python2. Pour python 3, voir ma réponse ci-dessous: stackoverflow.com/questions/14749328/…
R. Yang
1
C'est plutôt cool, mais il vaut mieux éviter le problème avec une meilleure conception en premier lieu, si possible.
Karol
@Karol, je suis d'accord. Dans la plupart des cas, cela ne devrait pas être nécessaire si la conception est raisonnable.
Ellioh
4

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, donc je peux être très confiant que lorsque je vois cette valeur pour arg, elle n'a pas été transmise.

Jesse B Miller
la source
4
Les objets Python sont des références, vous pouvez simplement les utiliser à la object()place de uuid4()- c'est toujours une instance unique , ce qui isvérifie
cz
3

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

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

DEFAULT = Default()

... ou l'équivalent monoliner ...:

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

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

cz
la source
3

La réponse de @ Ellioh fonctionne en python 2. En python 3, le code suivant devrait fonctionner:

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

Cette méthode peut conserver les noms d'argument et les valeurs par défaut (au lieu de ** kwargs) avec une meilleure lisibilité.

R. Yang
la source
3

Vous pouvez le vérifier depuis foo.__defaults__etfoo.__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 isvous pouvez les comparer

et dans certains cas, le code ci-dessous suffit

Par exemple, vous devez éviter de changer la valeur par défaut, vous pouvez vérifier l'égalité et copier si c'est le cas

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

De plus, il est assez pratique d'utiliser getcallargsfrom inspectcar il renvoie des arguments réels avec lesquels la fonction sera appelée. Vous lui passez une fonction et des args et kwargs ( inspect.getcallargs(func, /, *args, **kwds)), cela retournera les arguments de la méthode réelle utilisés pour l'invocation, en tenant compte des valeurs par défaut et d'autres choses. Jetez un œil à un exemple ci-dessous.

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

Alex
la source
2

Je suis d'accord avec le commentaire de Volatility. 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
isedev
la source
Je crois qu'un paramètre facultatif est quelque chose comme def func(optional=value)non**kwargs
Zaur Nasibov
C'est quelque chose qui est quelque peu sujet à interprétation. Quelle est la différence réelle entre un argument avec une valeur par défaut et un argument de mot-clé? Ils sont tous deux exprimés en utilisant la même syntaxe «mot-clé = valeur».
isedev
Je ne suis pas d'accord, car le but des paramètres facultatifs et **kwargsest un peu différent. PS pas de problème à propos de -1 :) Et mon -1 pour vous était accidentel :)
Zaur Nasibov
2

Ceci est une variation de la réponse de Stefano, mais je trouve un peu plus lisible:

not_specified = {}

def foo(x=not_specified):
    if x is not_specified:
            print("not specified")
    else:
            print("specified")
Lazieburd
la source
Un vote positif ?? J'aime ça le plus. Simple, pas de réflexion. Lisible.
vincent le
1

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 mentionné, c'est assez bizarre, mais cela fait le travail. Cependant, ceci est tout à fait illisible et de la même manière que la suggestion d' ecatmur ne sera pas automatiquement documentée.

dmg
la source
1
Vous voudrez peut-être inclure le comportement de check_f('z'), qui est aussi, comme vous le dites, bizarre.
Michael
@ MichaelJ.Barber Bon point. Vous devrez également faire de la "magie" avec * args. Cependant, mon point était que c'est possible, mais avoir besoin maintenant de savoir si la valeur par défaut est passée ou non est une mauvaise conception.
dmg