Python args converter para kwargs
Pergunta
Eu estou escrevendo um decorador que precisa chamar outras funções antes de chamar a função que ele está decorando. A função decorado pode ter argumentos posicionais, mas as funções do decorador vai chamar só pode aceitar argumentos de palavra-chave. Alguém tem uma maneira prática de conversão de argumentos posicionais em argumentos de palavra-chave?
Eu sei que posso obter uma lista dos nomes de variáveis ??da função decorado:
>>> def a(one, two=2):
... pass
>>> a.func_code.co_varnames
('one', 'two')
Mas eu não consigo descobrir como dizer o que foi passado no posicionalmente eo que era como palavra-chave.
As minhas decorador esta aparência:
class mydec(object):
def __init__(self, f, *args, **kwargs):
self.f = f
def __call__(self, *args, **kwargs):
hozer(**kwargs)
self.f(*args, **kwargs)
Existe uma maneira que não seja apenas comparando kwargs e co_varnames, somando-se kwargs qualquer coisa não lá, e esperando o melhor?
Solução
Nota - co_varnames irá incluir variáveis ??locais, bem como palavras-chave. Isso provavelmente não importa, como zip trunca a seqüência mais curta, mas pode resultar em confundir mensagens de erro se você passar o número errado de argumentos.
Você pode evitar isso com func_code.co_varnames[:func_code.co_argcount]
, mas melhor é usar a inspecionar módulo . ou seja:
import inspect
argnames, varargs, kwargs, defaults = inspect.getargspec(func)
Você também pode querer lidar com o caso em que as define a função **kwargs
ou *args
(mesmo que apenas para gerar uma exceção quando usado com o decorador). Se estes estiverem definidos, o segundo e terceiro resultado de getargspec
retornará seu nome variável, caso contrário eles serão Nenhum.
Outras dicas
Qualquer arg que foi passado posicionalmente será passado para args *. E qualquer arg passado como uma palavra-chave será passado para kwargs **. Se você tem valores e nomes de argumentos posicionais, em seguida, você pode fazer:
kwargs.update(dict(zip(myfunc.func_code.co_varnames, args)))
para convertê-los todos em argumentos de palavra-chave.
Se você estiver usando Python> = 2,7 inspect.getcallargs()
faz isso para você fora da caixa. Você tinha acabado de passar a função decorado como o primeiro argumento, e depois o resto dos argumentos exatamente como você pretende chamá-lo. Exemplo:
>>> def f(p1, p2, k1=None, k2=None, **kwargs):
... pass
>>> from inspect import getcallargs
Eu estou planejando fazer f('p1', 'p2', 'p3', k2='k2', extra='kx1')
(note que k1 está sendo passado posicionalmente como P3), então ...
>>> call_args = getcallargs(f, 'p1', 'p2', 'p3', k2='k2', extra='kx1')
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'kwargs': {'extra': 'kx1'}}
Se você sabe a função decorado não vai usar **kwargs
, em seguida, essa chave não aparecerá no dict, e está feito (e eu estou supondo que não há nenhuma *args
, uma vez que iria quebrar a exigência de que tudo o que tem um nome). Se você do tem **kwargs
, como eu tenho neste exemplo, e quiser incluí-los com o resto dos argumentos nomeados, é preciso mais uma linha:
>>> call_args.update(call_args.pop('kwargs'))
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'extra': 'kx1'}
Update: para Python> = 3.3, consulte inspect.Signature.bind()
e o relacionado inspect.signature
função para a funcionalidade semelhante (mas mais robusto do que) inspect.getcallargs()
.
Bem, isso pode ser um exagero. Escrevi-o para o pacote dectools (em PyPI), para que possa obter atualizações lá. Ele retorna a tomada dicionário em conta posicional, palavra-chave e argumentos padrão. Há um conjunto de testes no pacote (test_dict_as_called.py):
def _dict_as_called(function, args, kwargs):
""" return a dict of all the args and kwargs as the keywords they would
be received in a real function call. It does not call function.
"""
names, args_name, kwargs_name, defaults = inspect.getargspec(function)
# assign basic args
params = {}
if args_name:
basic_arg_count = len(names)
params.update(zip(names[:], args)) # zip stops at shorter sequence
params[args_name] = args[basic_arg_count:]
else:
params.update(zip(names, args))
# assign kwargs given
if kwargs_name:
params[kwargs_name] = {}
for kw, value in kwargs.iteritems():
if kw in names:
params[kw] = value
else:
params[kwargs_name][kw] = value
else:
params.update(kwargs)
# assign defaults
if defaults:
for pos, value in enumerate(defaults):
if names[-len(defaults) + pos] not in params:
params[names[-len(defaults) + pos]] = value
# check we did it correctly. Each param and only params are set
assert set(params.iterkeys()) == (set(names)|set([args_name])|set([kwargs_name])
)-set([None])
return params
Aqui está um novo método para resolver isso usando inspect.signature
(para Python 3.3+). Vou dar um exemplo que pode ser executado / testou-se primeiro e depois mostrar como modificar o código original com ele.
Aqui está uma função de teste que apenas resume quaisquer args / kwargs que lhe são atribuídas; pelo menos um argumento é necessário (a
) e há um argumento só de palavra-chave com um valor padrão (b
), apenas para testar diferentes aspectos de assinaturas de função.
def silly_sum(a, *args, b=1, **kwargs):
return a + b + sum(args) + sum(kwargs.values())
Agora vamos fazer um wrapper para silly_sum
que pode ser chamado da mesma forma como silly_sum
(com uma exceção que nós vamos chegar a), mas que só passa em kwargs ao silly_sum
embrulhado.
def wrapper(f):
sig = inspect.signature(f)
def wrapped(*args, **kwargs):
bound_args = sig.bind(*args, **kwargs)
bound_args.apply_defaults()
print(bound_args) # just for testing
all_kwargs = bound_args.arguments
assert len(all_kwargs.pop("args")) == 0
all_kwargs.update(all_kwargs.pop("kwargs"))
return f(**all_kwargs)
return wrapped
sig.bind
retorna um objeto BoundArguments
, mas isso não leva em conta o padrão a menos que você chamar apply_defaults
explicitamente. Isso também irá gerar uma tupla vazia para argumentos e um dict vazio para kwargs se nenhum *args
/ **kwargs
foram dadas.
sum_wrapped = wrapper(silly_sum)
sum_wrapped(1, c=9, d=11)
# prints <BoundArguments (a=1, args=(), b=1, kwargs={'c': 9, 'd': 11})>
# returns 22
Depois, é só pegar o dicionário de argumentos e adicionar qualquer **kwargs
. A exceção a usar esse wrapper é que *args
não pode ser passado para a função. Isso ocorre porque não há nomes para estes, por isso não podemos convertê-los em kwargs. Se passando-os através de um kwarg argumentos nomeados é aceitável, que poderia ser feito em seu lugar.
Aqui está como isso pode ser aplicado ao código original:
import inspect
class mydec(object):
def __init__(self, f, *args, **kwargs):
self.f = f
self._f_sig = inspect.signature(f)
def __call__(self, *args, **kwargs):
bound_args = self._f_sig.bind(*args, **kwargs)
bound_args.apply_defaults()
all_kwargs = bound_args.arguments
assert len(all_kwargs.pop("args")) == 0
all_kwargs.update(all_kwargs.pop("kwargs"))
hozer(**all_kwargs)
self.f(*args, **kwargs)