So überprüfen Sie, ob der optionale Funktionsparameter eingestellt ist

90

Gibt es in Python eine einfache Möglichkeit zu überprüfen, ob der Wert eines optionalen Parameters von seinem Standardwert stammt oder ob der Benutzer ihn beim Funktionsaufruf explizit festgelegt hat?

Matthias
quelle
12
Weil ich es natürlich in dieser Funktion überprüfen möchte :)
Matthias
2
Verwenden Sie einfach Noneals Standard und überprüfen Sie dies. Wenn Sie diesen Test wirklich einrichten könnten, würden Sie auch jede Möglichkeit für den Benutzer ausschließen, den Wert, der das Standardverhalten aufruft, explizit zu übergeben.
Michael J. Barber
3
Dies kann viel wiederverwendbarer und schöner sein als die Antwort, die Sie akzeptiert haben, zumindest für CPython. Siehe meine Antwort unten.
Ellioh
2
@Volatility: Es ist wichtig, wenn Sie zwei Standardeinstellungen haben. Stellen Sie sich eine rekursive Klasse vor: Der Class My(): def __init__(self, _p=None, a=True, b=True, c=False) Benutzer ruft sie mit auf x=My(b=False). Eine Klassenmethode könnte sich selbst aufrufen, x=My(_p=self, c=True)wenn Funktionen erkennen könnten, dass b nicht explizit gesetzt ist und dass nicht gesetzte Variablen von der obersten Ebene weitergegeben werden sollen. Wenn dies nicht möglich ist, müssen die rekursiven Aufrufe jede Variable explizit übergeben : x=My(a=self.a, b=self.b, c=True, d=self.d, ...).
Dave
@ Dave, aber geht es darum? In meinem Verständnis bittet die Frage , wie zu unterscheiden x=My()und x=My(a=True). In Ihrem Szenario wird optionalen Parametern ein anderer Wert als der Standardwert zugewiesen.
Volatilität

Antworten:

15

Viele Antworten enthalten kleine Teile der vollständigen Informationen, daher möchte ich alles mit meinen Lieblingsmustern zusammenbringen.

Standardwert ist ein mutableTyp

Wenn der Standardwert ein veränderbares Objekt ist, haben Sie Glück: Sie können die Tatsache ausnutzen, dass die Standardargumente von Python bei der Definition der Funktion einmal ausgewertet werden (mehr dazu am Ende der Antwort im letzten Abschnitt).

Dies bedeutet, dass Sie einen veränderlichen Standardwert leicht vergleichen können, indem Sie feststellen is, ob er als Argument übergeben oder standardmäßig belassen wurde, wie in den folgenden Beispielen als Funktion oder Methode:

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

und

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

Unveränderliche Standardargumente

Jetzt ist es etwas weniger elegant, wenn erwartet wird, dass Ihre Standardeinstellung ein immutableWert ist (und denken Sie daran, dass sogar Zeichenfolgen unveränderlich sind!), Weil Sie den Trick nicht so ausnutzen können, wie er ist, aber es gibt immer noch etwas, das Sie tun können, indem Sie immer noch veränderlich ausnutzen Art; Grundsätzlich setzen Sie einen veränderlichen "falschen" Standard in die Funktionssignatur und den gewünschten "echten" Standardwert in den Funktionskörper.

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)

Es fühlt sich besonders lustig an, wenn Sie eine echte Standardeinstellung haben None, diese aber Noneunveränderlich ist. Sie müssen also weiterhin explizit eine veränderbare Variable als Standardparameter für die Funktion verwenden und im Code zu Keine wechseln.

Verwenden einer DefaultKlasse für unveränderliche Standardeinstellungen

oder, ähnlich wie bei einem @ cz-Vorschlag, wenn Python-Dokumente nicht ausreichen :-), können Sie ein Objekt dazwischen hinzufügen, um die API expliziter zu gestalten (ohne die Dokumente zu lesen); Die Klasseninstanz used_proxy_ Default ist veränderbar und enthält den tatsächlichen Standardwert, den Sie verwenden möchten.

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

jetzt:

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

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

Das obige funktioniert auch gut für Default(None).

Andere Muster

Offensichtlich sehen die obigen Muster hässlicher aus als sie sollten, weil all printdiese nur dazu dienen, zu zeigen, wie sie funktionieren. Ansonsten finde ich sie knapp und wiederholbar genug.

Sie könnten einen Dekorateur schreiben, um das __call__von @dmg vorgeschlagene Muster rationaler hinzuzufügen , aber dies zwingt immer noch dazu, seltsame Tricks in der Funktionsdefinition selbst zu verwenden - Sie müssten sich aufteilen valueund value_defaultwenn Ihr Code sie unterscheiden muss, so Ich sehe keinen großen Vorteil und werde das Beispiel nicht schreiben :-)

Veränderbare Typen als Standardwerte in Python

Ein bisschen mehr über # 1 Python Gotcha! , zu Ihrem eigenen Vergnügen oben missbraucht. Sie können sehen, was aufgrund der Bewertung bei der Definition passiert, indem Sie Folgendes tun:

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

Sie können so oft ausführen, testme()wie Sie möchten. Es wird immer ein Verweis auf dieselbe Standardinstanz angezeigt (Ihre Standardeinstellung ist also im Grunde unveränderlich :-)).

Denken Sie daran , dass in Python gibt es nur 3 wandelbar eingebauten Typen : set, list, dict; alles andere - sogar Saiten! - ist unveränderlich.

Stefano
quelle
Das Beispiel in "Unveränderliche Standardargumente" enthält kein unveränderliches Standardargument. Wenn es so wäre, würde es nicht funktionieren.
Karol
@ Karol, willst du das näher erläutern? Der Standardwert in diesem Beispiel ist 1, der unveränderlich sein sollte ...
Stefano
Ich sehe die Signatur der Funktion als def f(value={}).
Karol
@Karol das ist die Signatur, aber der gewünschte Standardwert ist 1; Es tut mir leid, wenn dies in der Erklärung nicht klar ist, aber der Sinn dieses Teils der Antwort besteht darin, einen unveränderlichen Standardwert ( 1) zu haben. Wenn Sie das Beispiel überprüfen, werden Sie sehen, dass es sagt: print('default'); value = 1nichtvalue={}
Stefano
1
Ha, ich verstehe es jetzt, danke. Es ist nicht einfach zu folgen, wenn nicht jemand Ihren Text sehr sorgfältig liest, was bei SO möglicherweise nicht so häufig vorkommt. Erwägen Sie eine Neuformulierung.
Karol
56

Nicht wirklich. Die Standardmethode besteht darin, einen Standardwert zu verwenden, den der Benutzer voraussichtlich nicht übergeben wird, z. B. eine objectInstanz:

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

Normalerweise können Sie nur Noneals Standardwert verwenden, wenn dies als Wert, den der Benutzer übergeben möchte, keinen Sinn ergibt.

Die Alternative ist zu verwenden kwargs:

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

Dies ist jedoch zu ausführlich und erschwert die Verwendung Ihrer Funktion, da die Dokumentation den paramParameter nicht automatisch enthält .

ecatmur
quelle
8
Ich habe auch mehrere Leute gesehen, die die eingebaute Ellipse für Orte verwenden, an denen dies erforderlich ist, und Keine wird als gültige Eingabe angesehen. Dies entspricht im Wesentlichen dem ersten Beispiel.
GrandOpener
Wenn Sie ein spezielles Verhalten implementieren möchten, wenn None übergeben wurde, aber dennoch eine Möglichkeit benötigen, um zu testen, ob das Argument vom Benutzer angegeben wurde, können Sie den EllipsisSingleton als Standard verwenden, der explizit als Überspringen dieses Werts konzipiert wurde. ...ist ein Alias ​​für Ellipsis, sodass Benutzer, die Positionsargumente verwenden möchten, einfach aufrufen können, your_function(p1, ..., p3)was das Lesen offensichtlich und angenehm macht.
Bachsau
However this is overly verbose and makes your function more difficult to use as its documentation will not automatically include the param parameter.Dies ist eigentlich nicht wahr, da Sie die Beschreibung einer Funktion und ihrer Parameter mithilfe des inspectModuls festlegen können . Es hängt von Ihrer IDE ab, ob es funktioniert oder nicht.
EZLearner
15

Der folgende Funktionsdekorator erstellt explicit_checkereine Reihe von Parameternamen für alle explizit angegebenen Parameter. Es fügt das Ergebnis als zusätzlichen Parameter ( explicit_params) zur Funktion hinzu. 'a' in explicit_paramsÜberprüfen Sie einfach , ob der Parameter aexplizit angegeben ist.

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
quelle
Dieser Code funktioniert nur in Python2. Für Python 3 siehe meine Antwort unten: stackoverflow.com/questions/14749328/…
R. Yang
1
Das ist ziemlich cool, aber besser, um das Problem mit besserem Design zu vermeiden, wenn möglich.
Karol
@ Karol, ich stimme zu. In den meisten Fällen sollte man das nicht brauchen, wenn das Design angemessen ist.
Ellioh
4

Ich verwende manchmal eine universell eindeutige Zeichenfolge (wie eine UUID).

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

Auf diese Weise konnte kein Benutzer den Standard erraten, wenn er es versuchte, sodass ich sehr sicher sein kann, dass dieser Wert, wenn ich ihn sehe, argnicht übergeben wurde.

Jesse B. Miller
quelle
4
Python-Objekte sind Referenzen, die Sie einfach verwenden können, object()anstatt uuid4()- es ist immer noch eine eindeutige Instanz , die isüberprüft wird
cz
3

Ich habe dieses Muster ein paar Mal (zB Bibliothek gesehen unittest, py-flags, jinja):

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

DEFAULT = Default()

... oder der entsprechende Einzeiler ...:

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

Im Gegensatz dazu DEFAULT = object()unterstützt dies die Typprüfung und liefert Informationen, wenn Fehler auftreten. In Fehlermeldungen wird häufig entweder die Zeichenfolgendarstellung ( "DEFAULT") oder der Klassenname ( "Default") verwendet.

cz
quelle
3

@ Elliohs Antwort funktioniert in Python 2. In Python 3 sollte der folgende Code funktionieren:

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

Diese Methode kann die Argumentnamen und Standardwerte (anstelle von ** kwargs) mit besserer Lesbarkeit beibehalten.

R. Yang
quelle
3

Sie können es von foo.__defaults__und überprüfenfoo.__kwdefaults__

siehe ein einfaches Beispiel unten

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

dann mit dem Operator =und isSie können sie vergleichen

und in einigen Fällen ist der folgende Code unten ausreichend

Sie müssen beispielsweise vermeiden, den Standardwert zu ändern, dann können Sie die Gleichheit überprüfen und dann kopieren, wenn dies der Fall ist

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

Auch ist es sehr bequem zu bedienen getcallargsaus , inspectda es gibt echte Argumente , mit denen Funktion aufgerufen wird. Wenn Sie eine Funktion und Argumente und Warnungen an it übergeben ( inspect.getcallargs(func, /, *args, **kwds)), werden die Argumente der realen Methode zurückgegeben, die für den Aufruf verwendet werden, wobei Standardwerte und andere Elemente berücksichtigt werden. Schauen Sie sich unten ein Beispiel an.

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

Ich stimme dem Kommentar von Volatility zu. Sie können dies jedoch auf folgende Weise überprüfen:

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
quelle
Ich glaube, ein optionaler Parameter ist so etwas wie def func(optional=value)nicht**kwargs
Zaur Nasibov
Das ist etwas, das etwas offen für Interpretationen ist. Was ist der tatsächliche Unterschied zwischen einem Argument mit einem Standardwert und einem Schlüsselwortargument? Sie werden beide mit derselben Syntax "keyword = value" ausgedrückt.
Isedev
Ich bin anderer Meinung, weil der Zweck der optionalen Parameter und **kwargsetwas anders ist. PS kein Problem ist über -1 :) Und meine -1 für Sie war zufällig :)
Zaur Nasibov
2

Dies ist eine Variation von Stefanos Antwort, aber ich finde sie etwas lesbarer:

not_specified = {}

def foo(x=not_specified):
    if x is not_specified:
            print("not specified")
    else:
            print("specified")
lazieburd
quelle
Eine Gegenstimme? Das gefällt mir am besten. Einfach, keine Reflexion. Lesbar.
Vincent
1

Ein etwas verrückter Ansatz wäre:

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

Welche Ausgänge:

passed default
z
passed different
b
not passed
z

Nun, das ist, wie ich schon sagte, ziemlich verrückt, aber es macht den Job. Dies ist jedoch ziemlich unlesbar und wird ähnlich wie der Vorschlag von ecatmur nicht automatisch dokumentiert.

dmg
quelle
1
Vielleicht möchten Sie das Verhalten von check_f('z')einbeziehen, das, wie Sie sagen, auch verrückt ist.
Michael J. Barber
@ MichaelJ.Barber Guter Punkt. Sie müssen auch mit * args etwas "Magie" machen. Mein Punkt war jedoch, dass es möglich ist, aber jetzt zu müssen, ob der Standardwert übergeben wird oder nicht, ist ein schlechtes Design.
dmg