Como verificar se o parâmetro de função opcional está definido

93

Existe uma maneira fácil em Python de verificar se o valor de um parâmetro opcional vem de seu valor padrão ou porque o usuário o definiu explicitamente na chamada de função?

Matthias
fonte
12
Porque quero verificar nessa função, é claro :)
Matthias
2
Basta usar Nonecomo padrão e verificar isso. Se você realmente pudesse configurar esse teste, também excluiria qualquer possibilidade de o usuário passar explicitamente o valor que invoca o comportamento padrão.
Michael J. Barber
3
Isso pode ser feito de uma forma muito mais reutilizável e bonita do que uma resposta que você aceitou, pelo menos para o CPython. Veja minha resposta abaixo.
Ellioh
2
@Volatilidade: importa se você tem dois conjuntos de padrões. Considere uma classe recursiva: o Class My(): def __init__(self, _p=None, a=True, b=True, c=False) usuário a chama com x=My(b=False). Um método de classe poderia chamar a si mesmo com x=My(_p=self, c=True)se as funções pudessem detectar que b não está explicitamente definido e que as variáveis ​​não definidas devem ser transmitidas do nível superior. Mas se eles não podem, as chamadas recursivas tem que passar todas as variáveis explicitamente: x=My(a=self.a, b=self.b, c=True, d=self.d, ...).
Dave
@Dave mas é disso que se trata? No meu entendimento, a questão é como diferenciar x=My()e x=My(a=True). Seu cenário envolve a atribuição de parâmetros opcionais a um valor diferente de seu valor padrão.
Volatilidade

Respostas:

19

Muitas respostas contêm pequenos pedaços de informações completas, então eu gostaria de juntar tudo com meu (s) padrão (ões) favorito (s).

o valor padrão é um mutable tipo

Se o valor padrão for um objeto mutável, você tem sorte: você pode explorar o fato de que os argumentos padrão do Python são avaliados uma vez quando a função é definida (mais informações sobre isso no final da resposta na última seção)

Isso significa que você pode comparar facilmente um valor mutável padrão usando ispara ver se ele foi passado como um argumento ou deixado como padrão, como nos exemplos a seguir como função ou método:

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

e

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

Argumentos padrão imutáveis

Agora, é um pouco menos elegante se for esperado que seu padrão seja um immutablevalor (e lembre-se de que até mesmo strings são imutáveis!) Porque você não pode explorar o truque como ele é, mas ainda há algo que você pode fazer, ainda explorando mutável tipo; basicamente, você coloca um padrão "falso" mutável na assinatura da função e o valor padrão "real" desejado no corpo da função.

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)

É particularmente engraçado se o seu padrão real for None, masNone é imutável, então ... você ainda precisa usar explicitamente um mutável como o parâmetro padrão da função e alternar para Nenhum no código.

Usando um Default classe para padrões imutáveis

ou, semelhante à sugestão do @cz, se os documentos do python não forem suficientes :-), você pode adicionar um objeto no meio para tornar a API mais explícita (sem ler os documentos); a instância da classe used_proxy_ Default é mutável e conterá o valor padrão real que você deseja 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))

agora:

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

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

O acima também funciona bem para Default(None).

Outros padrões

Obviamente, os padrões acima parecem mais feios do que deveriam por causa de todos os printque existem apenas para mostrar como funcionam. Caso contrário, acho que são concisos e repetíveis o suficiente.

Você poderia escrever um decorador para adicionar o __call__padrão sugerido por @dmg de uma forma mais simplificada, mas isso ainda obrigará o uso de truques estranhos na própria definição da função - você precisaria separar valueevalue_default se seu código precisar distingui-los, Não vejo muita vantagem e não vou escrever o exemplo :-)

Tipos mutáveis ​​como valores padrão em Python

Um pouco mais sobre # 1 python gotcha! , abusado para seu próprio prazer acima. Você pode ver o que acontece devido à avaliação na definição , fazendo:

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

Você pode executar testme()quantas vezes quiser, você sempre verá uma referência à mesma instância padrão (então, basicamente, seu padrão é imutável :-)).

Lembre-se que em Python existem apenas 3 mutável built-in tipos : set, list, dict; tudo o mais - até cordas! - é imutável.

Stefano
fonte
O exemplo que você tem em "Argumentos padrão imutáveis" não possui, na verdade, um argumento padrão imutável. Se assim fosse, não funcionaria.
Karol
@Karol, gostaria de elaborar? O valor padrão nesse exemplo é 1, que deve ser imutável ...
Stefano
Eu vejo a assinatura da função como def f(value={}).
Karol
1
Ha, eu entendo agora, obrigado. Não é fácil de seguir a menos que alguém leia seu texto com muito cuidado, o que pode não acontecer com tanta frequência no SO. Considere reformular.
Karol
1
Em "if default is f .__ defaults __ [0]:", você tem que codificar qual número de parâmetro default usar, que pode ser frágil se a assinatura da função mudar. Uma alternativa é "se padrão em f .__ defaults__:". Supondo que você use uma instância padrão diferente para cada arg, "in" deve funcionar tão bem quanto "is".
Stephen Warren
57

Na verdade não. A forma padrão é usar um valor padrão que o usuário não deveria passar, por exemplo, uma objectinstância:

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

Normalmente, você pode usar apenas Nonecomo o valor padrão, se não fizer sentido como um valor que o usuário deseja passar.

A alternativa é usar kwargs:

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

No entanto, isso é muito detalhado e torna sua função mais difícil de usar, pois a documentação não inclui o paramparâmetro automaticamente .

ecatmur
fonte
9
Também vi várias pessoas usarem o interno Ellipsis para locais onde isso é necessário e nenhum é considerado uma entrada válida. Este é essencialmente o mesmo que o primeiro exemplo.
GrandOpener
Se você deseja implementar um comportamento especial se Nenhum foi passado, mas ainda precisa de uma maneira de testar se o argumento foi fornecido pelo usuário, você pode usar o Ellipsissingleton como um padrão, que foi explicitamente projetado para ser usado como um salto desse valor. ...é um apelido para Ellipsis, então os usuários que desejam usar argumentos posicionais podem apenas chamar, o your_function(p1, ..., p3)que torna a leitura óbvia e agradável.
Bachsau de
However this is overly verbose and makes your function more difficult to use as its documentation will not automatically include the param parameter.Na verdade, isso não é verdade, pois você pode definir a descrição de uma função e de seus parâmetros usando o inspectmódulo. Depende do seu IDE se vai funcionar ou não.
EZLearner
15

O seguinte decorador de função,, explicit_checkercria um conjunto de nomes de parâmetro de todos os parâmetros fornecidos explicitamente. Ele adiciona o resultado como um parâmetro extra ( explicit_params) à função. Basta fazer 'a' in explicit_paramspara verificar se o parâmetro aé fornecido explicitamente.

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
fonte
Este código funciona apenas em python2. Para python 3, veja minha resposta abaixo: stackoverflow.com/questions/14749328/…
R. Yang
1
Isso é muito legal, mas é melhor evitar o problema com um design melhor em primeiro lugar, se possível.
Karol
@Karol, eu concordo. Na maioria dos casos, não será necessário se o projeto for razoável.
Ellioh
4

Às vezes, uso uma string universalmente exclusiva (como um UUID).

import uuid
DEFAULT = uuid.uuid4()
def foo(arg=DEFAULT):
  if arg is DEFAULT:
    # it was not passed in
  else:
    # it was passed in

Dessa forma, nenhum usuário poderia adivinhar o padrão se tentasse, então posso ter certeza de que, quando vejo esse valor para arg, ele não foi transmitido.

Jesse B Miller
fonte
4
Os objetos Python são referências, você pode simplesmente usar em object()vez de uuid4()- ainda é uma instância única , que é o que isverifica
cz
3

Eu já vi esse padrão algumas vezes (ou seja, bibliotecas unittest, py-flags, jinja):

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

DEFAULT = Default()

... ou equivalente de uma linha ...:

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

Ao contrário DEFAULT = object(), isso auxilia na verificação de tipo e fornece informações quando ocorrem erros - frequentemente, a representação da string ( "DEFAULT") ou o nome da classe ( "Default") são usados ​​em mensagens de erro.

cz
fonte
3

A resposta de @Ellioh funciona em python 2. Em python 3, o seguinte código deve 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 pode manter os nomes dos argumentos e valores padrão (em vez de ** kwargs) com melhor legibilidade.

R. Yang
fonte
3

Você pode verificar em foo.__defaults__ efoo.__kwdefaults__

veja um exemplo simples abaixo

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

então usando o operador = e isvocê pode compará-los

e para alguns casos o código abaixo é suficiente

Por exemplo, você precisa evitar alterar o valor padrão, então você pode verificar a igualdade e, em seguida, copiar se assim for

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

Além disso, é bastante conveniente usar getcallargsfrom inspect, pois retorna argumentos reais com os quais a função será chamada. Você passa uma função e args e kwargs para ele ( inspect.getcallargs(func, /, *args, **kwds)), ele retornará os argumentos do método real usados ​​para invocação, levando em consideração os valores padrão e outras coisas. Dê uma olhada no exemplo abaixo.

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

Eu concordo com o comentário do Volatility. Mas você pode verificar da seguinte maneira:

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
fonte
Eu acredito que um parâmetro opcional é algo como def func(optional=value)não**kwargs
Zaur Nasibov
Isso é algo que pode ser interpretado. Qual é a diferença real entre um argumento com um valor padrão e um argumento de palavra-chave? Ambos são expressos usando a mesma sintaxe "palavra-chave = valor".
isedev
Eu discordo, porque a finalidade dos parâmetros opcionais e **kwargsé um pouco diferente. PS sem problemas é sobre -1 :) E meu -1 para você foi acidental :)
Zaur Nasibov
2

Esta é uma variação da resposta de Stefano, mas acho um pouco mais legível:

not_specified = {}

def foo(x=not_specified):
    if x is not_specified:
            print("not specified")
    else:
            print("specified")
preguiçoso
fonte
Um voto positivo ?? Eu gosto mais disso. Simples, sem reflexão. Legível.
Vincent
1

Uma abordagem um pouco bizarra seria:

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

Quais saídas:

passed default
z
passed different
b
not passed
z

Agora, isso, como mencionei, é bastante estranho, mas faz o trabalho. No entanto, este é bastante ilegível e à semelhança do ecatmur 's sugestão não será documentado automaticamente.

dmg
fonte
1
Você pode querer incluir o comportamento de check_f('z'), que também é, como você diz, bizarro.
Michael J. Barber
@ MichaelJ.Barber Bom ponto. Você terá que fazer alguma "mágica" com * args também. No entanto, meu ponto é que é possível, mas precisar saber se o valor padrão foi passado ou não é um projeto ruim.
dmg