Получение аргументов ключевого слова, фактически переданных методу Python

Я мечтаю о методе Python с явными аргументами ключевого слова:

def func(a=None, b=None, c=None):
    for arg, val in magic_arg_dict.items():   # Where do I get the magic?
        print '%s: %s' % (arg, val)

Я хочу получить словарь только тех аргументов, которые вызывающий объект фактически передал в метод, точно так же, как **kwargs, но я не хочу, чтобы вызывающий объект мог передавать какие-либо старые случайные аргументы, в отличие от **kwargs.

>>> func(b=2)
b: 2
>>> func(a=3, c=5)
a: 3
c: 5

Итак: есть ли такое заклинание? В моем случае мне довелось сравнить каждый аргумент с аргументом по умолчанию, чтобы найти те, которые отличаются, но это отчасти неэлегантно и становится утомительным, когда у вас есть девять аргументов. Для бонусных баллов предоставьте заклинание, которое может сказать мне, даже когда вызывающий передает аргумент ключевого слова, которому присвоено значение по умолчанию:

>>> func(a=None)
a: None

Хитрый!

Изменить: (лексическая) сигнатура функции должна оставаться неизменной. Это часть общедоступного API, и основная ценность явных аргументов ключевого слова заключается в их документальной ценности. Просто чтобы было интересно. :)


person Doctor J    schedule 11.09.2009    source источник
comment
У этого вопроса очень похожий заголовок, но я не совсем уверен, что это дубликат: stackoverflow.com/questions/196960/   -  person Anderson Green    schedule 01.07.2013
comment
@AndersonGreen. Вопрос, о котором вы говорите, не имеет к этому никакого отношения. Он спрашивает, как фильтровать словарь при передаче с ** нотацией методу, который не принимает все ключевые слова.   -  person Mad Physicist    schedule 14.12.2015
comment
Это большой вопрос.   -  person Mad Physicist    schedule 14.12.2015


Ответы (8)


Я был вдохновлен добротой декоратора lost-theory, и, немного поигравшись с ней, пришел к следующему:

def actual_kwargs():
    """
    Decorator that provides the wrapped function with an attribute 'actual_kwargs'
    containing just those keyword arguments actually passed in to the function.
    """
    def decorator(function):
        def inner(*args, **kwargs):
            inner.actual_kwargs = kwargs
            return function(*args, **kwargs)
        return inner
    return decorator


if __name__ == "__main__":

    @actual_kwargs()
    def func(msg, a=None, b=False, c='', d=0):
        print msg
        for arg, val in sorted(func.actual_kwargs.iteritems()):
            print '  %s: %s' % (arg, val)

    func("I'm only passing a", a='a')
    func("Here's b and c", b=True, c='c')
    func("All defaults", a=None, b=False, c='', d=0)
    func("Nothin'")
    try:
        func("Invalid kwarg", e="bogon")
    except TypeError, err:
        print 'Invalid kwarg\n  %s' % err

Что печатает это:

I'm only passing a
  a: a
Here's b and c
  b: True
  c: c
All defaults
  a: None
  b: False
  c: 
  d: 0
Nothin'
Invalid kwarg
  func() got an unexpected keyword argument 'e'

Я доволен этим. Более гибкий подход - передать имя атрибута, который вы хотите использовать, декоратору, вместо того, чтобы жестко кодировать его как «actual_kwargs», но это самый простой подход, который иллюстрирует решение.

Ммм, Python вкусный.

person Doctor J    schedule 11.09.2009
comment
Хех, неплохо. Мне тоже нравится этот метод. Спасибо! - person Maiku Mori; 11.09.2009

Вот самый простой и простой способ:

def func(a=None, b=None, c=None):
    args = locals().copy()
    print args

func(2, "egg")

Это даст результат: {'a': 2, 'c': None, 'b': 'egg'}. Причина, по которой args должен быть копией словаря locals, заключается в том, что словари изменяемы, поэтому, если вы создали какие-либо локальные переменные в этой функции, args будет содержать все локальные переменные и их значения, а не только аргументы.

Дополнительную документацию по встроенной функции locals см. здесь.

person Isaiah    schedule 11.09.2009
comment
Красиво - и близко - но c не должно быть там. Он не говорит мне, какие из них вы прошли. - person Doctor J; 11.09.2009

Одна возможность:

def f(**kw):
  acceptable_names = set('a', 'b', 'c')
  if not (set(kw) <= acceptable_names):
    raise WhateverYouWantException(whatever)
  ...proceed...

IOW, очень легко проверить, что переданные имена находятся в допустимом наборе, и в противном случае поднять все, что вы хотите, чтобы Python поднял (TypeError, я думаю ;-). Кстати, довольно легко превратиться в декоратора.

Другая возможность:

_sentinel = object():
def f(a=_sentinel, b=_sentinel, c=_sentinel):
   ...proceed with checks `is _sentinel`...

создавая уникальный объект _sentinel, вы устраняете риск того, что вызывающий объект может случайно передать None (или другие неуникальные значения по умолчанию, которые вызывающий может передать). Это все, для чего object() подходит, кстати: чрезвычайно легкий, уникальный дозорный, который невозможно случайно спутать с каким-либо другим объектом (когда вы проверяете с помощью оператора is).

Любое решение предпочтительнее для немного разных проблем.

person Alex Martelli    schedule 11.09.2009

Как насчет использования декоратора для проверки входящих kwargs?

def validate_kwargs(*keys):
    def entangle(f):
        def inner(*args, **kwargs):
            for key in kwargs:
                if not key in keys:
                    raise ValueError("Received bad kwarg: '%s', expected: %s" % (key, keys))
            return f(*args, **kwargs)
        return inner
    return entangle

###

@validate_kwargs('a', 'b', 'c')
def func(**kwargs):
   for arg,val in kwargs.items():
       print arg, "->", val

func(b=2)
print '----'
func(a=3, c=5)
print '----'
func(d='not gonna work')

Дает такой вывод:

b -> 2
----
a -> 3
c -> 5
----
Traceback (most recent call last):
  File "kwargs.py", line 20, in <module>
    func(d='not gonna work')
  File "kwargs.py", line 6, in inner
    raise ValueError("Received bad kwarg: '%s', expected: %s" % (key, keys))
ValueError: Received bad kwarg: 'd', expected: ('a', 'b', 'c')
person Steven Kryskalla    schedule 11.09.2009

Проще всего это сделать с помощью одного экземпляра сторожевого объекта:

# Top of module, does not need to be exposed in __all__
missing = {}

# Function prototype
def myFunc(a = missing, b = missing, c = missing):
    if a is not missing:
        # User specified argument a
    if b is missing:
        # User did not specify argument b

Хорошая вещь в этом подходе заключается в том, что, поскольку мы используем оператор «is», вызывающий может передать пустой dict в качестве значения аргумента, и мы все равно поймем, что они не хотели его передавать. Мы также избегаем неприятных декораторов и делаем наш код немного чище.

person Walt W    schedule 08.05.2012
comment
Поскольку на самом деле пропущенный объект не является диктовкой, почему бы не использовать object ()? Это уникальный (то есть основной) экземпляр объекта. Не используется (без обмана) ни для чего. О, и del отсутствует в конце модуля, поэтому его нельзя экспортировать, если вы хотите быть в безопасности. - person Jürgen A. Erhard; 17.02.2013
comment
Вы не можете его удалить; функциям необходимо, чтобы он находился в глобальной области видимости, поскольку они ссылаются на него оттуда. Вы можете поставить перед ним знак подчеркивания, если действительно беспокоитесь. Object или dict сработают; Я не знаю о каких-либо серьезных преимуществах того и другого, кроме того, что {} более кратко, чем object (). - person Walt W; 26.02.2013

Вероятно, есть способы сделать это лучше, но вот мой вывод:

def CompareArgs(argdict, **kwargs):
    if not set(argdict.keys()) <= set(kwargs.keys()):
        # not <= may seem weird, but comparing sets sometimes gives weird results.
        # set1 <= set2 means that all items in set 1 are present in set 2
        raise ValueError("invalid args")

def foo(**kwargs):
    # we declare foo's "standard" args to be a, b, c
    CompareArgs(kwargs, a=None, b=None, c=None)
    print "Inside foo"


if __name__ == "__main__":
    foo(a=1)
    foo(a=1, b=3)
    foo(a=1, b=3, c=5)
    foo(c=10)
    foo(bar=6)

и его вывод:

Inside foo
Inside foo
Inside foo
Inside foo
Traceback (most recent call last):
  File "a.py", line 18, in 
    foo(bar=6)
  File "a.py", line 9, in foo
    CompareArgs(kwargs, a=None, b=None, c=None)
  File "a.py", line 5, in CompareArgs
    raise ValueError("invalid args")
ValueError: invalid args

Возможно, его можно было бы преобразовать в декоратор, но моим декораторам нужно поработать. Оставлено читателю в качестве упражнения: P

person Mark Rushakoff    schedule 11.09.2009

Возможно, возникнет ошибка, если они передадут какие-либо аргументы * args?

def func(*args, **kwargs):
  if args:
    raise TypeError("no positional args allowed")
  arg1 = kwargs.pop("arg1", "default")
  if kwargs:
    raise TypeError("unknown args " + str(kwargs.keys()))

Было бы просто учесть это, взяв список имен переменных или общую функцию синтаксического анализа для использования. Не составит труда превратить это в декоратор (python 3.1):

def OnlyKwargs(func):
  allowed = func.__code__.co_varnames
  def wrap(*args, **kwargs):
    assert not args
    # or whatever logic you need wrt required args
    assert sorted(allowed) == sorted(kwargs)
    return func(**kwargs)

Примечание: я не уверен, насколько хорошо это будет работать с уже обернутыми функциями или функциями, которые уже имеют *args или **kwargs.

person Richard Levasseur    schedule 11.09.2009

Магия - это не ответ:

def funky(a=None, b=None, c=None):
    for name, value in [('a', a), ('b', b), ('c', c)]:
        print name, value
person Lennart Regebro    schedule 11.09.2009
comment
...Что ты хочешь этим сказать)? он же человек, я не понимаю - person n611x007; 10.06.2015