¿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?
python
function
optional-parameters
Matías
fuente
fuente
None
como 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.Class My(): def __init__(self, _p=None, a=True, b=True, c=False)
usuario la llama conx=My(b=False)
. Un método de clase podría llamarse a sí mismo conx=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, ...)
.x=My()
yx=My(a=True)
. Su escenario implica asignar a los parámetros opcionales un valor que no sea su valor predeterminado.Respuestas:
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
mutable
tipoSi 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
is
para 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
immutable
valor (¡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
, peroNone
es 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
Default
clase para valores predeterminados inmutableso, 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
print
cuales 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 dividirsevalue
yvalue_default
si 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.fuente
1
, que debería ser inmutable ...def f(value={})
.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 = 1
novalue={}
Realmente no. La forma estándar es utilizar un valor predeterminado que no se espera que el usuario pase, por ejemplo, una
object
instancia:DEFAULT = object() def foo(param=DEFAULT): if param is DEFAULT: ...
Por lo general, puede usarlo
None
como 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
param
parámetro.fuente
Ellipsis
singleton como predeterminado, que se diseñó explícitamente para omitir este valor....
es un alias paraEllipsis
, 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.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 elinspect
módulo. Depende de su IDE si funcionará o no.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_params
para verificar si el parámetroa
se 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)
fuente
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ó.fuente
object()
en lugar deuuid4()
- sigue siendo una única instancia , que es lo queis
los chequesHe visto este patrón varias veces (por ejemplo
unittest
, bibliotecapy-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.fuente
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.
fuente
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
=
yis
puedes compararlosy 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
getcallargs
frominspect
ya 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
fuente
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
fuente
def func(optional=value)
no**kwargs
**kwargs
es un poco diferente. PD: no hay problema con -1 :) Y mi -1 para ti fue accidental :)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")
fuente
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.
fuente
check_f('z')
, que también es, como usted dice, anormal.