¿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?
fuente
¿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?
Muchas respuestas tienen pequeños fragmentos de la información completa, así que me gustaría reunirlo todo junto con mis patrones favoritos.
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')
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
, pero None
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.
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)
.
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 dividirse value
y value_default
si su código necesita distinguirlos, entonces No veo mucha ventaja y no escribiré el ejemplo :-)
¡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.
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.
Ellipsis
singleton 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.
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 inspect
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ámetro a
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)
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ó.
object()
en lugar de uuid4()
- sigue siendo una única instancia , que es lo que is
los cheques
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.
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.
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 is
puedes 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 getcallargs
from inspect
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}
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
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")
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.
check_f('z')
, que también es, como usted dice, anormal.
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.