Как проверить, установлен ли необязательный параметр функции

93

Есть ли в Python простой способ проверить, исходит ли значение необязательного параметра из его значения по умолчанию или потому, что пользователь явно установил его при вызове функции?

Матиас
источник
12
Потому что я, конечно, хочу проверить это в этой функции :)
Маттиас
2
Просто используйте Noneпо умолчанию и проверьте это. Если бы вы действительно могли настроить этот тест, вы также исключили бы любую возможность для пользователя явно передать значение, которое вызывает поведение по умолчанию.
Майкл Дж. Барбер
3
Это можно сделать гораздо более удобным и красивым способом, чем в ответе, который вы приняли, по крайней мере, для CPython. Смотрите мой ответ ниже.
Ellioh 07
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, ...).
Дэйв
@ Дэйв, но в этом ли вопрос? В моем понимании вопрос заключается в том, как различать x=My()и x=My(a=True). В вашем сценарии необязательным параметрам присваивается значение, отличное от значения по умолчанию.
Volatility

Ответы:

19

Многие ответы содержат небольшие фрагменты полной информации, поэтому я хотел бы собрать все это вместе с моими любимыми шаблонами.

значение по умолчанию - это 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_ Default является изменяемым и будет содержать реальное значение по умолчанию, которое вы хотите использовать.

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! , оскорбляли для собственного удовольствия выше. Вы можете увидеть, что происходит из-за оценки при определении , выполнив:

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

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

Помните , что в Python есть только 3 изменяемые встроенных типов : set, list, dict; все остальное - даже струны! - неизменен.

Стефано
источник
В примере, приведенном в разделе «Неизменяемые аргументы по умолчанию», на самом деле нет неизменяемого аргумента по умолчанию. Если бы это было так, ничего бы не вышло.
Кароль
@ Кароль, уточни? Значение по умолчанию в этом примере 1, которое должно быть неизменным ...
Стефано
Я вижу сигнатуру функции как def f(value={}).
Кароль
1
Ха, теперь я понял, спасибо. За ним нелегко следить, если только кто-то не прочитает ваш текст очень внимательно, что может случиться не так часто на SO. Попробуйте перефразировать.
Karol
1
В «если по умолчанию f .__ defaults __ [0]:» вы должны жестко указать, какой номер параметра по умолчанию использовать, что может оказаться нестабильным при изменении сигнатуры функции. Альтернативой является «если по умолчанию в f .__ defaults__:». Предполагая, что вы используете разные экземпляры по умолчанию для каждого аргумента, "in" должно работать так же хорошо, как и "is".
Стивен Уоррен
57

На самом деле, нет. Стандартный способ - использовать значение по умолчанию, которое пользователь не может передать, например 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параметр.

Ecatmur
источник
9
Я также видел, как несколько человек использовали встроенный Ellipsis там, где это необходимо, и None считается допустимым вводом. По сути, это то же самое, что и в первом примере.
GrandOpener
Если вы хотите реализовать специальное поведение, если было передано значение None, но все же вам нужен способ проверить, был ли аргумент предоставлен пользователем, вы можете использовать Ellipsisсинглтон по умолчанию, который был явно разработан для использования в качестве пропуска этого значения. ...- это псевдоним для Ellipsis, поэтому пользователи, которые хотят использовать позиционные аргументы, могут просто вызвать, your_function(p1, ..., p3)что делает его очевидным и приятным для чтения.
Bachsau
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)
Ellioh
источник
Этот код работает только в python2. Для python 3 см. Мой ответ ниже: stackoverflow.com/questions/14749328/…
Р. Ян
1
Это довольно круто, но лучше, если возможно, вообще избежать проблем с улучшенным дизайном.
Кароль
@Karol, я согласен. В большинстве случаев в этом нет необходимости, если дизайн разумный.
Ellioh
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").

cz
источник
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

Alex
источник
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 07
Я не согласен, потому что назначение необязательных параметров и **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 «s предложение не будет автоматически документированы.

dmg
источник
1
Возможно, вы захотите включить поведение пользователя check_f('z'), которое также, как вы говорите, странно.
Майкл Дж. Барбер,
@ MichaelJ.Barber Хорошее замечание. Вам также придется сделать некоторую "магию" с * args. Тем не менее, я хотел сказать, что это возможно, но необходимость выяснять, передается ли значение по умолчанию или нет, - это плохой дизайн.
dmg