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?

Foi útil?

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)
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top