how to create a new method with signature of anoth

2019-04-30 02:55发布

问题:

How can I copy the signature of a method from one class, and create a "proxy method" with same signature in another ?.

I am writing a RPC library in python. The server supports remote calls to a server-side class (C). When the client connects to the server, it should create a proxy class for C with same signatures. When the program calls proxy instance, it should call the function with same arguments at the server.

回答1:

Consider using boltons.wraps - here's an excerpt from the documentation:

boltons.funcutils.wraps(func, injected=None, **kw)

Modeled after the built-in functools.wraps(), this function is used to make your decorator’s wrapper functions reflect the wrapped function’s:

Name Documentation Module Signature

The built-in functools.wraps() copies the first three, but does not copy the signature. This version of wraps can copy the inner function’s signature exactly, allowing seamless usage and introspection. Usage is identical to the built-in version:

>>> from boltons.funcutils import wraps
>>>
>>> def print_return(func):
...     @wraps(func)
...     def wrapper(*args, **kwargs):
...         ret = func(*args, **kwargs)
...         print(ret)
...         return ret
...     return wrapper
...
>>> @print_return
... def example():
...     '''docstring'''
...     return 'example return value'
>>>
>>> val = example()
example return value
>>> example.__name__
'example'
>>> example.__doc__
'docstring'

In addition, the boltons version of wraps supports modifying the outer signature based on the inner signature. By passing a list of injected argument names, those arguments will be removed from the outer wrapper’s signature, allowing your decorator to provide arguments that aren’t passed in.

Parameters: func (function) – The callable whose attributes are to be copied.

injected (list) – An optional list of argument names which should not appear in the new wrapper’s signature.

update_dict (bool) – Whether to copy other, non-standard attributes of func over to the wrapper. Defaults to True.

inject_to_varkw (bool) – Ignore missing arguments when a **kwargs-type catch-all is present. Defaults to True.

For more in-depth wrapping of functions, see the FunctionBuilder type, on which wraps was built.



回答2:

You don't have to copy the function signature. Instead, accept arbitrary positional and keyword arguments and pass those on:

def proxy_function(*args, **kw):
    return original_function(*args, **kw)

Here the *args and **kw syntax in the proxy_function signature are given a tuple and dictionary, respectively, of arguments passed into the function:

>>> def foo(*args, **kw):
...     print args
...     print kw
... 
>>> foo('spam', 'ham', monty='python')
('spam', 'ham')
{'monty': 'python'}

Similarly, the *args and **kw syntax in the original_function() call takes a sequence or a mapping, respectively, to apply their contents as separate arguments to the function being called:

>>> def bar(baz, fourtytwo=42):
...     print baz
...     print fourtytwo
... 
>>> args = ('hello world!',)
>>> kwargs = {'fourtytwo': 'the answer'}
>>> bar(*args, **kwargs)
hello world!
the answer

Combined, the two serve as an arbitrary-argument-pass-through for proxy functions.

Creating a full facade on the other hand is a little more involved:

import inspect

_default = object()

def build_facade(func):
    """Build a facade function, matching the signature of `func`.

    Note that defaults are replaced by _default, and _proxy will reconstruct
    these to preserve mutable defaults.

    """
    name = func.__name__
    docstring = func.__doc__
    spec = inspect.getargspec(func)
    args, defaults = spec[0], spec[3]
    boundmethod = getattr(func, '__self__', None)

    arglen = len(args)
    if defaults is not None:
        defaults = zip(args[arglen - len(defaults):], defaults)
        arglen -= len(defaults)

    def _proxy(*args, **kw):
        if boundmethod:
            args = args[1:]  # assume we are bound too and don't pass on self
        # Reconstruct keyword arguments
        if defaults is not None:
            args, kwparams = args[:arglen], args[arglen:]
            for positional, (key, default) in zip(kwparams, defaults):
                if positional is _default:
                    kw[key] = default
                else:
                    kw[key] = positional
        return func(*args, **kw)

    args = inspect.formatargspec(formatvalue=lambda v: '=_default', *spec)
    callargs = inspect.formatargspec(formatvalue=lambda v: '', *spec)

    facade = 'def {}{}:\n    """{}"""\n    return _proxy{}'.format(
        name, args, docstring, callargs)
    facade_globs = {'_proxy': _proxy, '_default': _default}
    exec facade in facade_globs
    return facade_globs[name]

This produces a whole new function object with the same argument names, and handles defaults by referencing the original proxied function defaults rather than copy them to the facade; this ensures that even mutable defaults continue to work.

The facade builder handles bound methods too; in that case self is removed from the call before passing on, to ensure that the target method is not getting an extra self argument (which would be the wrong type anyway).

Handling unbound methods is out of scope here; provide your own _proxy function in that case that can instantiate the proxied class and pass on arguments without the self or provide a new value for self; you cannot pass in self unaltered.

Demo:

>>> def foobar(bar, baz, default=[]):
...     print bar, baz, default
... 
>>> build_facade(foobar)
<function foobar at 0x10258df50>
>>> build_facade(foobar)('spam', 'eggs')
spam eggs []
>>> inspect.getargspec(build_facade(foobar))
ArgSpec(args=['bar', 'baz', 'default'], varargs=None, keywords=None, defaults=(<object object at 0x102593cd0>,))
>>> class Foo(object):
...     def bar(self, spam): print spam
... 
>>> foo = Foo()
>>> class FooProxy(object):
...     bar = build_facade(foo.bar)
... 
>>> FooProxy().bar('hello!')
hello!
>>> inspect.getargspec(FooProxy.bar)
ArgSpec(args=['self', 'spam'], varargs=None, keywords=None, defaults=None)