Jak sprawdzić, czy ustawiono opcjonalny parametr funkcji

90

Czy w Pythonie istnieje łatwy sposób sprawdzenia, czy wartość parametru opcjonalnego pochodzi z jego wartości domyślnej, czy też dlatego, że użytkownik ustawił ją jawnie przy wywołaniu funkcji?

Matthias
źródło
12
Bo chcę to oczywiście sprawdzić w tej funkcji :)
Matthias
2
Po prostu użyj Nonejako domyślnego i sprawdź to. Gdybyś naprawdę mógł skonfigurować ten test, wykluczyłbyś również jakąkolwiek możliwość jawnego przekazania przez użytkownika wartości, która wywołuje domyślne zachowanie.
Michael J. Barber
3
Można to zrobić o wiele piękniej i bardziej wielokrotnego użytku niż w odpowiedzi, którą zaakceptowałeś, przynajmniej w przypadku CPythona. Zobacz moją odpowiedź poniżej.
Ellioh,
2
@Volatility: ma znaczenie, jeśli masz dwa zestawy wartości domyślnych. Rozważmy klasę rekurencyjną: Class My(): def __init__(self, _p=None, a=True, b=True, c=False) użytkownik wywołuje ją za pomocą x=My(b=False). Metoda klasy mogłaby wywołać samą siebie z, x=My(_p=self, c=True)gdyby funkcje mogły wykryć, że b nie jest jawnie ustawione i że nieustawione zmienne mają być przekazywane z najwyższego poziomu. Ale jeśli nie, wywołania rekurencyjne muszą przejść każdą zmienną wyraźnie: x=My(a=self.a, b=self.b, c=True, d=self.d, ...).
Dave,
@Dave, ale czy o to chodzi? W moim rozumieniu pytanie brzmi: jak odróżnić x=My()i x=My(a=True). Twój scenariusz obejmuje przypisanie opcjonalnym parametrom wartości innej niż ich wartość domyślna.
Zmienność

Odpowiedzi:

15

Wiele odpowiedzi zawiera małe fragmenty pełnych informacji, więc chciałbym zebrać to wszystko razem z moimi ulubionymi wzorami.

wartością domyślną jest mutabletyp

Jeśli wartością domyślną jest obiekt zmienny, masz szczęście: możesz wykorzystać fakt, że domyślne argumenty Pythona są oceniane tylko raz, gdy funkcja jest zdefiniowana (więcej na ten temat na końcu odpowiedzi w ostatniej sekcji)

Oznacza to, że możesz łatwo porównać domyślną zmienną wartość za pomocą, isaby sprawdzić, czy została przekazana jako argument, czy pozostawiona domyślnie, jak w poniższych przykładach jako funkcja lub metoda:

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

i

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

Niezmienne argumenty domyślne

Teraz jest trochę mniej elegancko, jeśli domyślna wartość ma być immutablewartością (i pamiętaj, że nawet łańcuchy są niezmienne!), Ponieważ nie możesz wykorzystać tej sztuczki w obecnej postaci, ale wciąż jest coś, co możesz zrobić, wciąż wykorzystując zmienne rodzaj; zasadniczo umieszczasz zmienną „fałszywą” wartość domyślną w sygnaturze funkcji, a żądaną „rzeczywistą” wartość domyślną w treści funkcji.

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)

Wydaje się to szczególnie zabawne, jeśli prawdziwa wartość domyślna jest None, ale Nonejest niezmienna, więc ... nadal musisz jawnie użyć mutable jako domyślnego parametru funkcji i przełączyć się na None w kodzie.

Używanie Defaultklasy dla niezmiennych wartości domyślnych

lub, podobnie jak w przypadku sugestii @cz, jeśli dokumentacja Pythona nie wystarcza :-), możesz dodać obiekt pomiędzy nimi, aby API było bardziej przejrzyste (bez czytania dokumentacji); instancja klasy used_proxy_ Default jest zmienna i będzie zawierała rzeczywistą wartość domyślną, której chcesz użyć.

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

teraz:

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

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

Powyższe działa ładnie również w przypadku Default(None).

Inne wzory

Oczywiście powyższe wzorce wyglądają brzydiej niż powinny, ponieważ wszystkie printsą tylko po to, aby pokazać, jak działają. W przeciwnym razie uważam je za zwięzłe i wystarczająco powtarzalne.

Mógłbyś napisać dekorator, który doda __call__wzorzec sugerowany przez @dmg w bardziej uproszczony sposób, ale nadal będzie to obligowało do stosowania dziwnych sztuczek w samej definicji funkcji - musiałbyś się rozdzielić valuei value_defaultjeśli twój kod musi je rozróżnić, więc Nie widzę dużej przewagi i nie będę pisać przykładu :-)

Zmienne typy jako wartości domyślne w Pythonie

Trochę więcej o # 1 python gotcha! , nadużywane powyżej dla własnej przyjemności. Możesz zobaczyć, co dzieje się w wyniku oceny na definicji , wykonując:

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

Możesz uruchomić testme()tyle razy, ile chcesz, zawsze zobaczysz odniesienie do tej samej domyślnej instancji (więc w zasadzie twoja domyślna jest niezmienna :-)).

Pamiętaj, że w Pythonie istnieją tylko 3 zmienne wbudowane typy : set, list, dict; wszystko inne - nawet struny! - jest niezmienna.

Stefano
źródło
Przykład, który masz w „Niezmiennych argumentach domyślnych” w rzeczywistości nie ma niezmiennego argumentu domyślnego. Jeśli tak, to nie zadziała.
Karol
@Karol, chcesz to rozwinąć? Wartość domyślna w tym przykładzie to 1, która powinna być niezmienna ...
Stefano,
Widzę podpis funkcji jako def f(value={}).
Karol
@Karol to podpis, ale żądaną wartością domyślną jest 1; przepraszam, jeśli nie jest to jasne w wyjaśnieniu, ale celem tej części odpowiedzi jest możliwość posiadania niezmiennej wartości domyślnej ( 1). Jeśli spojrzysz na przykład, zobaczysz, że brzmi: print('default'); value = 1nievalue={}
Stefano
1
Ha, teraz rozumiem, dzięki. Nie jest łatwo śledzić, chyba że ktoś bardzo uważnie przeczyta Twój tekst, co może nie zdarzać się tak często w SO. Rozważ przeredagowanie.
Karol
56

Nie całkiem. Standardowym sposobem jest użycie wartości domyślnej, której użytkownik nie powinien podać, np. objectInstancja:

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

Zwykle możesz po prostu użyć Nonewartości domyślnej, jeśli nie ma to sensu jako wartości, którą użytkownik chciałby przekazać.

Alternatywą jest użycie kwargs:

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

Jest to jednak zbyt rozwlekłe i utrudnia korzystanie z funkcji, ponieważ jej dokumentacja nie będzie automatycznie zawierać paramparametru.

ecatmur
źródło
8
Widziałem również, że kilka osób używa wbudowanego wielokropka w miejscach, w których jest to potrzebne, a Żadne nie jest uważane za prawidłowe dane wejściowe. Jest to zasadniczo to samo, co w pierwszym przykładzie.
GrandOpener
Jeśli chcesz zaimplementować specjalne zachowanie, jeśli przekazano wartość None, ale nadal potrzebujesz sposobu na sprawdzenie, czy argument został podany przez użytkownika, możesz użyć Ellipsissingletona jako domyślnego, który został jawnie zaprojektowany do pominięcia tej wartości. ...jest aliasem dla Ellipsis, więc użytkownicy, którzy chcą używać argumentów pozycyjnych, mogą po prostu wywołać, your_function(p1, ..., p3)co sprawia, że ​​jest to oczywiste i przyjemne do czytania.
Bachsau
However this is overly verbose and makes your function more difficult to use as its documentation will not automatically include the param parameter.To faktycznie nieprawda, ponieważ opis funkcji i jej parametrów można ustawić za pomocą inspectmodułu. To zależy od twojego IDE, czy zadziała, czy nie.
EZLearner
15

Poniższy dekorator funkcji,, explicit_checkertworzy zestaw nazw parametrów wszystkich parametrów podanych jawnie. Dodaje wynik jako dodatkowy parametr ( explicit_params) do funkcji. Po prostu 'a' in explicit_paramssprawdź, czy parametr ajest podany jawnie.

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
źródło
Ten kod działa tylko w python2. W przypadku Pythona 3, zobacz moją odpowiedź poniżej: stackoverflow.com/questions/14749328/ ...
R. Yang
1
To całkiem fajne, ale lepiej uniknąć problemu z lepszym projektem, jeśli to możliwe.
Karol
@Karol, zgadzam się. W większości przypadków nie jest to konieczne, jeśli projekt jest rozsądny.
Ellioh,
4

Czasami używam uniwersalnego, unikalnego ciągu (takiego jak UUID).

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

W ten sposób żaden użytkownik nie mógł nawet odgadnąć wartości domyślnej, gdyby spróbował, więc mogę być bardzo pewien, że gdy widzę tę wartość arg, nie została ona przekazana.

Jesse B. Miller
źródło
4
Obiekty Pythona są referencjami, których możesz po prostu użyć object()zamiast nich uuid4()- to wciąż wyjątkowa instancja , co issprawdza
cz
3

Widziałem tego wzorca kilka razy (na przykład biblioteka unittest, py-flags, jinja):

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

DEFAULT = Default()

... lub równoważny jednolinijkowy ...:

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

W przeciwieństwie do DEFAULT = object()tego pomaga to w sprawdzaniu typu i dostarcza informacji, gdy wystąpią błędy - często w komunikatach o błędach używane są reprezentacja ciągu ( "DEFAULT") lub nazwa klasy ( "Default").

cz
źródło
3

Odpowiedź @ Ellioh działa w Pythonie 2. W Pythonie 3 następujący kod powinien działać:

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

Ta metoda może zachować nazwy argumentów i wartości domyślne (zamiast ** kwargs) z lepszą czytelnością.

R. Yang
źródło
3

Możesz to sprawdzić pod adresem foo.__defaults__ifoo.__kwdefaults__

zobacz prosty przykład poniżej

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

następnie używając operatora =i ismożesz je porównać

aw niektórych przypadkach kod poniżej jest wystarczający

Na przykład, musisz unikać zmiany wartości domyślnej, wtedy możesz sprawdzić równość, a następnie skopiować, jeśli tak

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

Jest również dość wygodny w użyciu getcallargsfrom, inspectponieważ zwraca rzeczywiste argumenty, z którymi funkcja zostanie wywołana. Przekazujesz do niej funkcję oraz argumenty i kwargi ( inspect.getcallargs(func, /, *args, **kwds)), zwróci ona prawdziwe argumenty metody użyte do wywołania, biorąc pod uwagę wartości domyślne i inne rzeczy. Spójrz na poniższy przykład.

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
źródło
2

Zgadzam się z komentarzem Volatility. Ale możesz sprawdzić w następujący sposób:

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
źródło
Uważam, że opcjonalny parametr jest czymś w rodzaju def func(optional=value)nie**kwargs
Zaur Nasibov,
To jest coś, co jest nieco otwarte na interpretację. Jaka jest rzeczywista różnica między argumentem z wartością domyślną a argumentem będącym słowem kluczowym? Oba są wyrażone przy użyciu tej samej składni „słowo kluczowe = wartość”.
isedev,
Nie zgadzam się, bo przeznaczenie parametrów opcjonalnych i **kwargsjest trochę inne. PS nie ma problemu z -1 :) A moje -1 dla Ciebie było przypadkowe :)
Zaur Nasibov
2

To jest wariacja na temat odpowiedzi Stefana, ale uważam, że jest trochę bardziej czytelna:

not_specified = {}

def foo(x=not_specified):
    if x is not_specified:
            print("not specified")
    else:
            print("specified")
lazieburd
źródło
Jeden głos za? To mi się najbardziej podoba. Proste, bez refleksji. Czytelny.
vincent
1

Trochę dziwacznym podejściem byłoby:

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

Które wyjścia:

passed default
z
passed different
b
not passed
z

Jak już wspomniałem, jest to dość dziwaczne, ale spełnia swoje zadanie. Jednak jest to dość nieczytelny i podobnie do ecatmur „s propozycja nie zostanie automatycznie udokumentowane.

dmg
źródło
1
Możesz uwzględnić zachowanie check_f('z'), które jest również, jak mówisz, dziwaczne.
Michael J. Barber,
@ MichaelJ.Barber Słuszna uwaga. Będziesz także musiał zrobić trochę "magii" z * argumentami. Chodziło mi jednak o to, że jest to możliwe, ale konieczność sprawdzenia, czy wartość domyślna jest przekazywana, czy nie, jest złym projektem.
dmg