Получение аргументов ключевого слова фактически передается в метод Python - PullRequest
22 голосов
/ 11 сентября 2009

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

Ответы [ 8 ]

25 голосов
/ 11 сентября 2009

Я был вдохновлен совершенством декоратора потерянной теории, и немного поиграв с этим, придумал:

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

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

15 голосов
/ 11 сентября 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 здесь .

5 голосов
/ 11 сентября 2009

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

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

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

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

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

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

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

2 голосов
/ 11 сентября 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')
1 голос
/ 08 мая 2012

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

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

0 голосов
/ 11 сентября 2009

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

def funky(a=None, b=None, c=None):
    for name, value in [('a', a), ('b', b), ('c', c)]:
        print name, value
0 голосов
/ 11 сентября 2009

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

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.

0 голосов
/ 11 сентября 2009

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

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

...