Cómo comprobar si el parámetro de función opcional está configurado

91

¿Existe una manera fácil en Python de verificar si el valor de un parámetro opcional proviene de su valor predeterminado, o porque el usuario lo estableció explícitamente en la llamada a la función?

Matías
fuente
12
Porque quiero verificarlo en esa función, por supuesto :)
Matthias
2
Solo utilícelo Nonecomo predeterminado y verifique eso. Si realmente pudiera configurar esta prueba, también excluiría cualquier posibilidad de que el usuario pase explícitamente el valor que invoca el comportamiento predeterminado.
Michael J. Barber
3
Eso se puede hacer de una manera mucho más reutilizable y hermosa que en la respuesta que aceptó, al menos para CPython. Vea mi respuesta a continuación.
Ellioh
2
@Volatilidad: importa si tiene dos conjuntos de valores predeterminados. Considere una clase recursiva: el Class My(): def __init__(self, _p=None, a=True, b=True, c=False) usuario la llama con x=My(b=False). Un método de clase podría llamarse a sí mismo con x=My(_p=self, c=True)si las funciones pudieran detectar que b no está establecido explícitamente y que las variables no configuradas deben transmitirse desde el nivel superior. Pero si no pueden, las llamadas recursivas tienen que pasar todas las variables explícitamente: x=My(a=self.a, b=self.b, c=True, d=self.d, ...).
Dave
@Dave, pero ¿de eso se trata la pregunta? A mi entender, la pregunta es cómo diferenciar x=My()y x=My(a=True). Su escenario implica asignar a los parámetros opcionales un valor que no sea su valor predeterminado.
Volatilidad

Respuestas:

16

Muchas respuestas tienen pequeños fragmentos de la información completa, así que me gustaría reunirlo todo junto con mis patrones favoritos.

el valor predeterminado es un mutabletipo

Si el valor predeterminado es un objeto mutable, tiene suerte: puede aprovechar el hecho de que los argumentos predeterminados de Python se evalúan una vez cuando se define la función (algo más sobre esto al final de la respuesta en la última sección)

Esto significa que puede comparar fácilmente un valor mutable predeterminado usando ispara ver si se pasó como un argumento o se dejó por defecto, como en los siguientes ejemplos como función o método:

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

y

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

Argumentos predeterminados inmutables

Ahora, es un poco menos elegante si se espera que su valor predeterminado sea un immutablevalor (¡y recuerde que incluso las cadenas son inmutables!) Porque no puede explotar el truco tal como está, pero todavía hay algo que puede hacer, aún explotando mutable tipo; Básicamente, pones un valor predeterminado "falso" mutable en la firma de la función y el valor predeterminado "real" deseado en el cuerpo de la función.

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)

Se siente particularmente divertido si su valor predeterminado real es None, pero Nonees inmutable, por lo que ... aún necesita usar explícitamente un mutable como parámetro predeterminado de la función y cambiar a Ninguno en el código.

Usando una Defaultclase para valores predeterminados inmutables

o, similar a la sugerencia de @cz, si los documentos de Python no son suficientes :-), puede agregar un objeto en el medio para hacer la API más explícita (sin leer los documentos); la instancia de la clase used_proxy_ Default es mutable y contendrá el valor predeterminado real que desea usar.

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

ahora:

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

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

Lo anterior también funciona muy bien para Default(None).

Otros patrones

Obviamente, los patrones anteriores se ven más feos de lo que deberían debido a todos los printcuales están ahí solo para mostrar cómo funcionan. De lo contrario, los encuentro concisos y lo suficientemente repetibles.

Podría escribir un decorador para agregar el __call__patrón sugerido por @dmg de una manera más simplificada, pero esto aún obligará a usar trucos extraños en la definición de la función en sí; necesitaría dividirse valuey value_defaultsi su código necesita distinguirlos, entonces No veo mucha ventaja y no escribiré el ejemplo :-)

Tipos mutables como valores predeterminados en Python

¡Un poco más sobre el número 1 de Python! , abusado para su propio placer arriba. Puede ver lo que sucede debido a la evaluación en la definición haciendo:

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

Puede ejecutar testme()tantas veces como desee, siempre verá una referencia a la misma instancia predeterminada (por lo que básicamente su valor predeterminado es inmutable :-)).

Recuerde que en Python sólo hay 3 mutable tipos incorporados : set, list, dict; todo lo demás, ¡incluso las cuerdas! - es inmutable.

Stefano
fuente
El ejemplo que tiene en "Argumentos predeterminados inmutables" en realidad no tiene un argumento predeterminado inmutable. Si lo hiciera, no funcionaría.
Karol
@Karol, ¿te importaría explicarlo? El valor predeterminado en ese ejemplo es 1, que debería ser inmutable ...
Stefano
Veo la firma de la función como def f(value={}).
Karol
@Karol esa es la firma, pero el valor predeterminado deseado es 1; lo siento si no está claro en la explicación, pero el objetivo de esa parte de la respuesta es poder tener un valor predeterminado inmutable ( 1). Si comprueba el ejemplo, verá que dice:, print('default'); value = 1novalue={}
Stefano
1
Ja, lo entiendo ahora, gracias. No es fácil de seguir a menos que alguien lea su texto con mucho cuidado, lo que podría no suceder con tanta frecuencia en SO. Considere la posibilidad de reformular.
Karol
56

Realmente no. La forma estándar es utilizar un valor predeterminado que no se espera que el usuario pase, por ejemplo, una objectinstancia:

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

Por lo general, puede usarlo Nonecomo valor predeterminado, si no tiene sentido como valor que el usuario querría pasar.

La alternativa es utilizar kwargs:

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

Sin embargo, esto es demasiado detallado y hace que su función sea más difícil de usar, ya que su documentación no incluirá automáticamente el paramparámetro.

ecatmur
fuente
8
También he visto a varias personas usar la Ellipsis incorporada para lugares donde se necesita y Ninguna se considera una entrada válida. Esto es esencialmente lo mismo que el primer ejemplo.
GrandOpener
Si desea implementar un comportamiento especial si se aprobó None, pero aún necesita una forma de probar si el usuario proporcionó el argumento, puede usar el Ellipsissingleton como predeterminado, que se diseñó explícitamente para omitir este valor. ...es un alias para Ellipsis, por lo que los usuarios que quieran usar argumentos posicionales pueden simplemente llamar, your_function(p1, ..., p3)lo que lo hace obvio y agradable de leer.
Bachsau
However this is overly verbose and makes your function more difficult to use as its documentation will not automatically include the param parameter.En realidad, esto no es cierto, ya que puede establecer la descripción de una función y de sus parámetros utilizando el inspectmódulo. Depende de su IDE si funcionará o no.
EZLearner
15

El siguiente decorador de funciones explicit_checker, hace un conjunto de nombres de parámetros de todos los parámetros dados explícitamente. Agrega el resultado como un parámetro adicional ( explicit_params) a la función. Solo hazlo 'a' in explicit_paramspara verificar si el parámetro ase proporciona explícitamente.

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
fuente
Este código solo funciona en python2. Para Python 3, vea mi respuesta a continuación: stackoverflow.com/questions/14749328/…
R. Yang
1
Esto es muy bueno, pero es mejor evitar el problema con un mejor diseño en primer lugar, si es posible.
Karol
@Karol, estoy de acuerdo. En la mayoría de los casos, no debería ser necesario si el diseño es razonable.
Ellioh
4

A veces utilizo una cadena única universal (como 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 esta manera, ningún usuario podría adivinar el valor predeterminado si lo intentara, por lo que puedo estar muy seguro de que cuando veo ese valor para arg, no se pasó.

Jesse B. Miller
fuente
4
Objetos de Python son referencias, sólo se puede utilizar object()en lugar de uuid4()- sigue siendo una única instancia , que es lo que islos cheques
cz
3

He visto este patrón varias veces (por ejemplo unittest, biblioteca py-flags, jinja):

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

DEFAULT = Default()

... o el equivalente one-liner ...:

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

A diferencia de DEFAULT = object()esto, esto ayuda a la verificación de tipos y proporciona información cuando ocurren errores; con frecuencia, la representación de la cadena ( "DEFAULT") o el nombre de la clase ( "Default") se utilizan en los mensajes de error.

cz
fuente
3

La respuesta de @ Ellioh funciona en python 2. En python 3, el siguiente código debería funcionar:

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

Este método puede mantener los nombres de los argumentos y los valores predeterminados (en lugar de ** kwargs) con una mejor legibilidad.

R. Yang
fuente
3

Puedes comprobarlo desde foo.__defaults__yfoo.__kwdefaults__

ver un ejemplo simple a continuación

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

luego usando el operador =y ispuedes compararlos

y para algunos casos el código a continuación es suficiente

Por ejemplo, debe evitar cambiar el valor predeterminado, luego puede verificar la igualdad y luego copiar si es así

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

Además, es bastante conveniente utilizar getcallargsfrom inspectya que devuelve argumentos reales con los que se invocará la función. Le pasa una función y args y kwargs ( inspect.getcallargs(func, /, *args, **kwds)), devolverá los argumentos del método real utilizados para la invocación, teniendo en cuenta los valores predeterminados y otras cosas. Eche un vistazo a un ejemplo a continuación.

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

Estoy de acuerdo con el comentario de Volatility. Pero puede verificar de la siguiente manera:

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
fuente
Creo que un parámetro opcional es algo así como def func(optional=value)no**kwargs
Zaur Nasibov
Eso es algo que está abierto a interpretaciones. ¿Cuál es la diferencia real entre un argumento con un valor predeterminado y un argumento de palabra clave? Ambos se expresan utilizando la misma sintaxis "palabra clave = valor".
isedev
No estoy de acuerdo, porque el propósito de los parámetros opcionales y **kwargses un poco diferente. PD: no hay problema con -1 :) Y mi -1 para ti fue accidental :)
Zaur Nasibov
2

Esta es una variación de la respuesta de stefano, pero encuentro un poco más legible:

not_specified = {}

def foo(x=not_specified):
    if x is not_specified:
            print("not specified")
    else:
            print("specified")
lazieburd
fuente
¿Un voto a favor? Me gusta esto más. Simple, sin reflejos. Legible.
Vincent
1

Un enfoque un poco extraño sería:

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

Qué salidas:

passed default
z
passed different
b
not passed
z

Ahora bien, esto, como mencioné, es bastante extraño, pero funciona. Sin embargo, esto es bastante ilegible y, al igual que la sugerencia de ecatmur , no se documentará automáticamente.

dmg
fuente
1
Es posible que desee incluir el comportamiento de check_f('z'), que también es, como usted dice, anormal.
Michael J. Barber
@ MichaelJ.Barber Buen punto. También tendrás que hacer algo de "magia" con * args. Sin embargo, mi punto fue que es posible, pero necesitar ahora si se pasa el valor predeterminado o no es un mal diseño.
dmg