Получение аргументов ключевого слова, фактически переданных методу 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, и основная ценность явных ключевых слов args заключается в их документальном значении. Просто чтобы все было интересно.:)

Ответ 1

Меня вдохновляла доброжелательность декораторов с теорией потери теории, и после того, как он немного поработал с этим, придумал следующее:

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 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 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", но это самый простой подход, который иллюстрирует решение.

Ммм, Питон вкусный.

Ответ 2

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

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 здесь.

Ответ 3

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

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() полезно для, btw: чрезвычайно легкий, уникальный дозор, который нельзя случайно спутать с каким-либо другим объектом (когда вы проверяете с помощью оператора is).

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

Ответ 4

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

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

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

Возможно, возникает ошибка, если они передают любые * 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()))

Было бы просто включить его в список списков varnames или общую функцию синтаксического анализа. Было бы не слишком сложно сделать это в декораторе (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.

Ответ 8

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

def funky(a=None, b=None, c=None):
    for name, value in [('a', a), ('b', b), ('c', c)]:
        print name, value