如何在对象上的多个方法上使用functools.partial,并且不按顺序冻结参数?

时间:2010-03-15 15:17:18

标签: functional-programming metaprogramming python standard-library

我发现functools.partial非常有用,但我希望能够不按顺序冻结参数(你要冻结的参数并不总是第一个)我希望能够申请它同时对一个类的几个方法,使一个代理对象具有与底层对象相同的方法,除了它的一些方法参数被冻结(将其视为概括部分以应用于类)。而且我更喜欢在不编辑原始对象的情况下执行此操作,就像partial不会更改其原始功能一样。

我设法废弃了一个名为'bind'的functools.partial版本,它允许我通过关键字参数传递它们来指定无序参数。那部分有效:

>>> def foo(x, y):
...     print x, y
...
>>> bar = bind(foo, y=3)
>>> bar(2)
2 3

但我的代理类不起作用,我不确定原因:

>>> class Foo(object):
...     def bar(self, x, y):
...             print x, y
...
>>> a = Foo()
>>> b = PureProxy(a, bar=bind(Foo.bar, y=3))
>>> b.bar(2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bar() takes exactly 3 arguments (2 given)

我可能会做各种各样的错误,因为我只是通过随机文档,博客和在所有部分上运行dir()拼凑而成。关于如何使这项工作和更好的方法来实现它的建议将不胜感激;)我不确定的一个细节是这应该如何与描述符互动。代码如下。

from types import MethodType

class PureProxy(object):
    def __init__(self, underlying, **substitutions):
        self.underlying = underlying

        for name in substitutions:
            subst_attr = substitutions[name]
            if hasattr(subst_attr, "underlying"):
                setattr(self, name, MethodType(subst_attr, self, PureProxy))

    def __getattribute__(self, name):
        return getattr(object.__getattribute__(self, "underlying"), name)

def bind(f, *args, **kwargs):
    """ Lets you freeze arguments of a function be certain values. Unlike
    functools.partial, you can freeze arguments by name, which has the bonus
    of letting you freeze them out of order. args will be treated just like
    partial, but kwargs will properly take into account if you are specifying
    a regular argument by name. """
    argspec = inspect.getargspec(f)
    argdict = copy(kwargs)

    if hasattr(f, "im_func"):
        f = f.im_func

    args_idx = 0
    for arg in argspec.args:
        if args_idx >= len(args):
            break

        argdict[arg] = args[args_idx]
        args_idx += 1

    num_plugged = args_idx

    def new_func(*inner_args, **inner_kwargs):
        args_idx = 0
        for arg in argspec.args[num_plugged:]:
            if arg in argdict:
                continue
            if args_idx >= len(inner_args):
                # We can't raise an error here because some remaining arguments
                # may have been passed in by keyword.
                break
            argdict[arg] = inner_args[args_idx]
            args_idx += 1

        f(**dict(argdict, **inner_kwargs))

    new_func.underlying = f

    return new_func

更新:如果有人可以受益,这是我最后的实施:

from types import MethodType

class PureProxy(object):
    """ Intended usage:
    >>> class Foo(object):
    ...     def bar(self, x, y):
    ...             print x, y
    ...
    >>> a = Foo()
    >>> b = PureProxy(a, bar=FreezeArgs(y=3))
    >>> b.bar(1)
    1 3
    """

    def __init__(self, underlying, **substitutions):
        self.underlying = underlying

        for name in substitutions:
            subst_attr = substitutions[name]
            if isinstance(subst_attr, FreezeArgs):
                underlying_func = getattr(underlying, name)
                new_method_func = bind(underlying_func, *subst_attr.args, **subst_attr.kwargs)
                setattr(self, name, MethodType(new_method_func, self, PureProxy))

    def __getattr__(self, name):
        return getattr(self.underlying, name)

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

def bind(f, *args, **kwargs):
    """ Lets you freeze arguments of a function be certain values. Unlike
    functools.partial, you can freeze arguments by name, which has the bonus
    of letting you freeze them out of order. args will be treated just like
    partial, but kwargs will properly take into account if you are specifying
    a regular argument by name. """
    argspec = inspect.getargspec(f)
    argdict = copy(kwargs)

    if hasattr(f, "im_func"):
        f = f.im_func

    args_idx = 0
    for arg in argspec.args:
        if args_idx >= len(args):
            break

        argdict[arg] = args[args_idx]
        args_idx += 1

    num_plugged = args_idx

    def new_func(*inner_args, **inner_kwargs):
        args_idx = 0
        for arg in argspec.args[num_plugged:]:
            if arg in argdict:
                continue
            if args_idx >= len(inner_args):
                # We can't raise an error here because some remaining arguments
                # may have been passed in by keyword.
                break
            argdict[arg] = inner_args[args_idx]
            args_idx += 1

        f(**dict(argdict, **inner_kwargs))

    return new_func

1 个答案:

答案 0 :(得分:3)

你的“绑定太深”:在课程def __getattribute__(self, name):中将def __getattr__(self, name):更改为PureProxy__getattribute__拦截每个属性访问权限,因此绕过您使用setattr(self, name, ...设置的所有内容,使得这些setattr失去任何效果,这显然不是您想要的; __getattr__仅用于访问未定义的属性,因此setattr次调用变为“有效”&amp;是有用的。

在覆盖的正文中,您可以也应该将object.__getattribute__(self, "underlying")更改为self.underlying(因为您不再覆盖__getattribute__)。我建议进行其他更改(enumerate代替您用于计数器的低级逻辑等),但它们不会改变语义。

根据我建议的更改,您的示例代码可以正常运行(当然,您必须继续使用更精细的案例进行测试)。顺便说一句,我调试它的方式只是在适当的地方坚持print语句(侏罗纪=时代的方法,但仍然是我的最爱; - )。