Як перевірити, чи встановлено необов’язковий параметр функції


92

Чи є простий спосіб у Python перевірити, чи походить значення необов’язкового параметра від значення за замовчуванням, або тому, що користувач явно встановив його під час виклику функції?


12
Тому що я хочу перевірити це в цій функції, звичайно :)
Маттіас

2
Просто використовуйте Noneза замовчуванням і перевірте це. Якщо ви дійсно могли налаштувати цей тест, ви також виключили будь-яку можливість для користувача явно передавати значення, яке викликає поведінку за замовчуванням.
Michael J. Barber

3
Це можна зробити набагато більше і красивіше, ніж у відповідь, яку ви прийняли, принаймні для CPython. Дивіться мою відповідь нижче.
Елліо

2
@Volatility: важливо, якщо у вас є два набори за замовчуванням. Розглянемо рекурсивний клас: Class My(): def __init__(self, _p=None, a=True, b=True, c=False) користувач викликає його за допомогою x=My(b=False). Метод класу може викликати себе, x=My(_p=self, c=True)якщо функції можуть виявити, що b не встановлено явно і що незмінні змінні повинні передаватися з верхнього рівня. Але якщо вони не можуть, рекурсивні виклики повинні проходити кожну змінну в явному вигляді: x=My(a=self.a, b=self.b, c=True, d=self.d, ...).
Дейв

@Dave, але чи це саме питання? На моє розуміння, питання полягає в тому, як розрізнити x=My()і x=My(a=True). Ваш сценарій передбачає присвоєння необов’язковим параметрам значення, відмінного від значення за замовчуванням.
Нестабільність

Відповіді:


16

Багато відповідей містять невеликі фрагменти повної інформації, тому я хотів би зібрати все разом із улюбленими шаблонами.

значення за замовчуванням - це mutableтип

Якщо значення за замовчуванням є змінним об'єктом, вам пощастить: ви можете використати той факт, що аргументи за замовчуванням Python обчислюються один раз, коли функція визначена (докладніше про це в кінці відповіді в останньому розділі)

Це означає, що ви можете легко порівняти значення, яке можна isзмінити за замовчуванням, використовуючи, щоб перевірити, чи було воно передано як аргумент чи залишено за замовчуванням, як у наступних прикладах як функція чи метод:

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

і

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

Незмінні аргументи за замовчуванням

Тепер це трохи менш елегантно, якщо очікується, що immutableзначенням за замовчуванням буде значення (і пам’ятайте, що навіть рядки незмінні!), Оскільки ви не можете використовувати фокус таким, як він є, але ви все ще можете щось зробити, все ще використовуючи змінні тип; в основному ви ставите змінний "підроблений" за замовчуванням у підпис функції та бажане "реальне" значення за замовчуванням у тілі функції.

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)

Це особливо смішно, якщо ти справжній за замовчуванням є None, але Noneє незмінним, тому ... тобі все одно потрібно явно використовувати мутабель як параметр функції за замовчуванням і перейти в значення None в коді.

Використання Defaultкласу для незмінних значень за замовчуванням

або, подібно до пропозиції @cz, якщо документів python недостатньо :-), ви можете додати об’єкт між ними, щоб зробити API більш явним (без читання документів); екземпляр класу used_proxy_ за замовчуванням змінюється і міститиме реальне значення за замовчуванням, яке ви хочете використовувати.

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

зараз:

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

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

Вищевказане добре працює і для Default(None).

Інші візерунки

Очевидно, що наведені вище схеми виглядають потворнішими, ніж мали б через усі, printякі існують лише для того, щоб показати, як вони працюють. В іншому випадку я вважаю їх стислими і досить повторюваними.

Ви можете написати декоратор, щоб додати __call__шаблон, запропонований @dmg, більш впорядкованим способом, але це все одно зобов’яже використовувати дивні трюки у самому визначенні функції - вам потрібно буде розділити, valueі value_defaultякщо ваш код повинен їх розрізняти, тому Я не бачу особливої ​​переваги, і я не буду писати приклад :-)

Змінні типи як значення за замовчуванням у Python

Трохи більше про # 1 python gotcha! , зловживання для вашого власного задоволення вище. Ви можете побачити, що відбувається завдяки оцінці при визначенні , виконавши:

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

Ви можете запускати testme()скільки завгодно часу, ви завжди побачите посилання на той самий екземпляр за замовчуванням (тому в основному ваше значення за замовчуванням незмінне :-)).

Пам'ятайте , що в Python є тільки 3 змінні вбудованих типів : set, list, dict; все інше - навіть струни! - незмінна.


Приклад у "Незмінні аргументи за замовчуванням" насправді не має незмінного аргументу за замовчуванням. Якби це сталося, це не спрацювало б.
Кароль

@Karol, докладно розробити? Значенням за замовчуванням у цьому прикладі є 1, яке повинно бути незмінним ...
Стефано

Я бачу підпис функції як def f(value={}).
Кароль

@Karol - це підпис, але бажаним значенням за замовчуванням є 1; вибачте, якщо це не зрозуміло в поясненні, але вся суть цієї частини відповіді полягає в можливості мати незмінний за замовчуванням ( 1). Якщо ви перевірите приклад, то побачите, що там написано:, а print('default'); value = 1неvalue={}
Стефано

1
Ха, я зрозумів зараз, дякую. Це непросто, якщо хтось не читає ваш текст дуже уважно, що може траплятися не так часто на SO. Подумайте про переформулювання.
Кароль

56

Не зовсім. Стандартним способом є використання значення за замовчуванням, яке користувач не повинен очікувати, наприклад, objectекземпляр:

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

Зазвичай ви можете просто використовувати Noneяк значення за замовчуванням, якщо це не має сенсу як значення, яке користувач бажає передати.

Альтернативою є використання kwargs:

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

Однак це занадто багатослівно і ускладнює використання вашої функції, оскільки її документація не включатиме paramпараметр автоматично .


9
Я також бачив, як кілька людей використовують вбудований Еліпсіс для місць, де це потрібно, і жоден не вважається дійсним введенням. Це, по суті, те саме, що і перший приклад.
GrandOpener

Якщо ви хочете реалізувати особливу поведінку, якщо не було передано None, але все-таки потрібен спосіб перевірити, чи аргумент був заданий користувачем, ви можете використовувати Ellipsisсинглтон за замовчуванням, який явно розроблений для використання як пропуск цього значення. ...є псевдонімом для Ellipsis, тому користувачі, які хочуть використовувати позиційні аргументи, можуть просто зателефонувати, your_function(p1, ..., p3)що робить його очевидним і приємним для читання.
Бахсау,

However this is overly verbose and makes your function more difficult to use as its documentation will not automatically include the param parameter.Це насправді неправда, оскільки ви можете встановити опис функції та її параметрів за допомогою inspectмодуля. Це буде працювати чи ні, залежить від вашої IDE.
EZLearner

15

Наступний декоратор функцій explicit_checker, робить набір імен параметрів усіх параметрів, явно заданих. Він додає результат як додатковий параметр ( explicit_params) до функції. Просто виконайте 'a' in explicit_paramsперевірку, якщо параметр aзадано явно.

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)

Цей код працює лише в python2. Щодо python 3, див. Мою відповідь нижче: stackoverflow.com/questions/14749328/…
Р. Янг

1
Це досить круто, але краще уникати проблем з першочерговим дизайном, якщо це можливо.
Кароль

@Karol, я згоден. У більшості випадків цього не потрібно, якщо дизайн обґрунтований.
Елліог,

4

Іноді я використовую універсальний унікальний рядок (наприклад, UUID).

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

Таким чином, жоден користувач навіть не вгадав за замовчуванням, якщо спробував, тому я можу бути дуже впевненим, що коли я бачу це значення для arg, воно не було передане.


4
Об'єкти Python - це посилання, ви можете просто використовувати їх object()замість uuid4()- це все ще унікальний екземпляр , саме це і isперевіряє
cz

3

Я бачив цю картину кілька разів (наприклад , бібліотека unittest, py-flags, jinja):

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

DEFAULT = Default()

... або еквівалентний однокласник ...:

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

На відміну від DEFAULT = object()цього, це допомагає перевіряти типи та надає інформацію при виникненні помилок - часто в повідомленнях про помилки використовується або рядкове представлення ( "DEFAULT"), або ім'я класу ( "Default").


3

Відповідь @ Ellioh працює в python 2. У python 3 повинен працювати наступний код:

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

Цей метод може зберігати імена аргументів та значення за замовчуванням (замість ** kwargs) з кращою читабельністю.


3

Ви можете перевірити це з foo.__defaults__іfoo.__kwdefaults__

див. простий приклад нижче

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

потім за допомогою оператора =і isви можете порівняти їх

а в деяких випадках достатній код нижче

Наприклад, вам потрібно уникати зміни значення за замовчуванням, тоді ви можете перевірити рівність, а потім скопіювати, якщо так

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

Крім того , це дуже зручно використовувати getcallargsв inspectякості повертає реальні аргументи , з допомогою якого функція буде викликатися. Ви передаєте їй функцію та args та kwargs ( inspect.getcallargs(func, /, *args, **kwds)), вона повертає аргументи реального методу, що використовуються для виклику, беручи до уваги значення за замовчуванням та інші речі. Погляньте на приклад нижче.

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


2

Я погоджуюся з коментарем Volatility. Але ви можете перевірити наступним чином:

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)ні**kwargs
Заур Насібов

Це те, що є дещо відкритим для інтерпретації. Яка фактична різниця між аргументом із значенням за замовчуванням та аргументом ключового слова? Вони обидва виражаються за допомогою одного синтаксису "ключове слово = значення".
isedev

Я не погоджуюсь, оскільки мета необов’язкових параметрів і **kwargsдещо інша. PS жодних проблем про -1 :) І мій -1 для вас був випадковим :)
Заур Насібов

2

Це варіація відповіді Стефано, але я вважаю трохи читабельнішим:

not_specified = {}

def foo(x=not_specified):
    if x is not_specified:
            print("not specified")
    else:
            print("specified")

Один голос за ?? Мені це подобається найбільше. Просто, без роздумів. Читається.
Вінсент

1

Трохи химерним підходом буде:

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

Які результати:

passed default
z
passed different
b
not passed
z

Зараз це, як я вже згадував, досить дивне, але воно робить свою справу. Однак це досить непрочитано , і, як і пропозиція ecatmur , не буде автоматично задокументовано.


1
Можливо, ви захочете включити поведінку check_f('z'), яка також, як ви кажете, чудернацька.
Michael J. Barber

@ MichaelJ.Barber Хороший момент. Вам також доведеться зробити «магію» з * аргументами. Однак я хотів сказати, що це можливо, але необхідність зараз передавати значення за замовчуванням чи ні - це поганий дизайн.
dmg
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.