如何创建具有另一个方法签名的新方法

6 投票
2 回答
2829 浏览
提问于 2025-04-18 18:16

我想知道怎么把一个类的方法签名复制过来,然后在另一个类里创建一个“代理方法”,让它的签名一样。

我正在用Python写一个RPC库。这个服务器支持对服务器端一个类(C)的远程调用。当客户端连接到服务器时,它应该为类C创建一个代理类,方法签名要一样。当程序调用这个代理实例时,它应该在服务器上用相同的参数调用那个方法。

2 个回答

2

你不需要完全复制函数的参数定义。相反,你可以接受任意数量的位置参数和关键字参数,然后把这些参数传递下去:

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

在这里,*args**kw 的写法在 proxy_function 的定义中,分别表示传入函数的参数会被处理成一个元组和一个字典:

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

同样,*args**kw 在调用 original_function() 时,分别接收一个序列和一个映射,用来将它们的内容作为单独的参数传递给被调用的函数:

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

结合起来,这两者可以让代理函数灵活地传递参数。

而创建一个完整的外观函数就稍微复杂一些:

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]

这样做会生成一个新的函数对象,参数名称与原来的相同,并且处理默认值时会引用原始被代理函数的默认值,而不是将它们复制到外观函数中;这样可以确保即使是可变的默认值也能正常工作。

外观构建器也能处理绑定方法;在这种情况下,self 会在传递之前被移除,以确保目标方法不会收到额外的 self 参数(而且这个参数类型本来就是错的)。

处理未绑定方法不在这里讨论;如果遇到这种情况,你需要提供自己的 _proxy 函数,这个函数可以实例化被代理的类,并在不传递 self 的情况下传递参数,或者提供一个新的 self 值;你不能直接传递未修改的 self

示例:

>>> 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)
4

可以考虑使用 boltons.wraps,以下是文档中的一段摘录:

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

这个函数是模仿内置的 functools.wraps(),用来让你的装饰器的包装函数能够反映被包装函数的特性:

名称 文档 模块 签名

内置的 functools.wraps() 会复制前面三个特性,但不会复制签名。而这个版本的 wraps 可以准确复制内部函数的签名,这样使用起来就更加顺畅,也更容易查看函数的细节。用法和内置版本是一样的:

>>> 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'

另外,boltons 版本的 wraps 还支持根据内部签名来修改外部签名。通过传入一个要注入的参数名称列表,这些参数就会从外部包装函数的签名中移除,这样你的装饰器就可以提供一些不需要传入的参数。

参数: func (function) – 要复制属性的可调用对象。

injected (list) – 一个可选的参数名称列表,这些名称不应该出现在新包装函数的签名中。

update_dict (bool) – 是否将 func 的其他非标准属性复制到包装函数中。默认为 True。

inject_to_varkw (bool) – 当存在 **kwargs 类型的捕获所有参数时,是否忽略缺失的参数。默认为 True。

如果想要更深入地了解函数的包装,可以查看 wraps 所基于的 FunctionBuilder 类型。

撰写回答