Frage

Ich schreibe ein Dekorateur, die andere Funktionen aufrufen muss vor der Funktion aufzurufen, die es schmückt. Die dekorierte Funktion kann Positionsargumente haben, aber die Funktionen der Dekorateur nennen kann nur Keyword-Argumente akzeptieren. Hat jemand eine praktische Möglichkeit, Positionsargumente in Keyword-Argumente der Umwandlung?

Ich weiß, dass ich eine Liste der Variablennamen der dekorierten Funktion bekommen:

>>> def a(one, two=2):
...    pass

>>> a.func_code.co_varnames
('one', 'two')

Aber ich kann nicht herausfinden, wie zu sagen, was in ihrer Position übergeben wurde und was als Schlüsselwort war.

Mein Dekorateur sieht wie folgt aus:

class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f

    def __call__(self, *args, **kwargs):
        hozer(**kwargs)
        self.f(*args, **kwargs)

Gibt es eine Möglichkeit andere als nur den Vergleich kwargs und co_varnames, und fügte hinzu, etwas kwargs nicht drin, und das Beste zu hoffen?

War es hilfreich?

Lösung

Hinweis - co_varnames werden lokale Variablen sowie Schlüsselwörter enthalten. Dies wird wahrscheinlich keine Rolle, wie zip die kürzere Sequenz abschneidet, aber in verwirrenden Fehlermeldungen führen kann, wenn Sie die falsche Anzahl von Argumenten übergeben.

Sie können dies vermeiden, mit func_code.co_varnames[:func_code.co_argcount], aber besser ist es, die Modul zu prüfen. dh:

import inspect
argnames, varargs, kwargs, defaults = inspect.getargspec(func)

Sie können auch den Fall behandeln wollen, wo die Funktion **kwargs oder *args (wenn auch nur zu erhöhen, eine Ausnahme, wenn sie mit dem Dekorateur verwendet) definiert. Wenn diese eingestellt sind, wird die zweite und dritte Folge von getargspec ihre Variablennamen zurückgeben, sonst werden sie keine sein.

Andere Tipps

Jede arg, die positionsübergeben wurde, wird an * args weitergegeben werden. Und jede arg als Schlüsselwort weitergegeben werden ** kwargs weitergegeben werden. Wenn Sie Positions args Werte und Namen haben, dann können Sie tun:

kwargs.update(dict(zip(myfunc.func_code.co_varnames, args)))

konvertieren sie alle in Schlüsselwort args.

Wenn Sie mit Python> = 2.7 inspect.getcallargs() tut dies für Sie aus dem Kasten heraus. Sie würden übergeben Sie einfach die eingerichtete Funktion als erstes Argument, und dann den Rest der Argumente genau so, wie Sie planen, es zu nennen. Beispiel:

>>> def f(p1, p2, k1=None, k2=None, **kwargs):
...     pass
>>> from inspect import getcallargs

Ich plane f('p1', 'p2', 'p3', k2='k2', extra='kx1') zu tun (beachten Sie, dass k1 positionell als p3 wird geleitet), so ...

>>> call_args = getcallargs(f, 'p1', 'p2', 'p3', k2='k2', extra='kx1')
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'kwargs': {'extra': 'kx1'}}

Wenn Sie die Funktion eingerichtet werden nicht wissen, **kwargs verwenden, wird dieser Schlüssel nicht in der dict erscheinen, und Sie sind fertig (und ich gehe davon aus, es gibt keine *args, denn das ist die Voraussetzung brechen würde, die alles haben ein Name). Wenn Sie tun **kwargs haben, wie ich in diesem Beispiel haben, und wollen, dass sie mit dem Rest der genannten Argumente enthalten, dauert es eine weitere Zeile:

>>> call_args.update(call_args.pop('kwargs'))
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'extra': 'kx1'}

Update: für Python> = 3.3 finden Sie unter inspect.Signature.bind() und die damit verbundene inspect.signature Funktion für ähnliche Funktionen wie (aber robuster als) inspect.getcallargs().

Nun, kann dies zu viel des Guten. Ich schrieb es für die dectools Paket (auf PyPI), so können Sie die Updates dort ankommen. Es gibt das Wörterbuch zu berücksichtigen Positions nehmen, Stichwort und Standardargumente. Es gibt eine Testsuite im Paket (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

Hier ist eine neuere Methode, um diese mit inspect.signature zu lösen (für Python 3.3+). Ich werde ein Beispiel geben, die ausgeführt werden können / getestet selbst zuerst und dann zeigen, wie der Original-Code mit, ihn zu ändern.

Hier ist eine Testfunktion, die fasst nur irgendwelche args / kwargs ihm gegeben; mindestens ein Argument erforderlich ist (a) und es gibt ein Schlüsselwort-Argument nur mit einem Standardwert (b), nur um zu testen, verschiedene Aspekte der Funktionssignaturen.

def silly_sum(a, *args, b=1, **kwargs):
    return a + b + sum(args) + sum(kwargs.values())

Lassen Sie uns nun einen Wrapper für silly_sum machen, die in der gleichen Weise wie silly_sum (mit einer Ausnahme, die wir bekommen) aufgerufen werden kann, aber das geht nur in kwargs zum gewickelt silly_sum.

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 gibt ein BoundArguments Objekt, aber diese Standardwerte nicht berücksichtigt, es sei denn, Sie apply_defaults explizit aufrufen. Dadurch wird auch ein leeres Tupel für args und eine leere dict für kwargs erzeugen, wenn kein *args / **kwargs gegeben wurde.

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

Dann erhalten wir nur das Wörterbuch von Argumenten und fügen Sie alle **kwargs in. Die Ausnahme dieser Wrapper ist, dass *args nicht an die Funktion übergeben werden. Dies ist, weil es keine Namen für diese sind, so können wir sie in kwargs nicht konvertieren. Wenn sie vorbei durch als kwarg namens args akzeptabel ist, dass stattdessen getan werden könnte.


Hier ist, wie sich dies auf den ursprünglichen Code angewendet werden:

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)
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top