【问题标题】:Proxy object in PythonPython中的代理对象
【发布时间】:2014-09-29 02:45:52
【问题描述】:

我正在寻找将方法调用从对象(包装器)传递到对象的成员变量(包装器)的方法。可能有许多方法需要外部化,因此在向 wrappee 添加方法时,在不更改包装器接口的情况下执行此操作的方法会很有帮助。

class Wrapper(object)
  def __init__(self, wrappee):
    self.wrappee = wrappee

  def foo(self):
    return 42

class Wrappee(object):
  def bar(self):
    return 12

o2 = Wrappee()
o1 = Wrapper(o2)

o1.foo() # -> 42
o1.bar() # -> 12
o1.<any new function in Wrappee>() # call directed to this new function 

如果此呼叫重定向“快速”(相对于直接呼叫,即不会增加太多开销),那就太好了。

【问题讨论】:

  • 为什么不继承?
  • 因为我在不同的上下文中单独使用这两个对象。

标签: python


【解决方案1】:

一个有点优雅的解决方案是在包装类上创建一个“属性代理”:

class Wrapper(object):
    def __init__(self, wrappee):
        self.wrappee = wrappee

    def foo(self):
        print 'foo'

    def __getattr__(self, attr):
        return getattr(self.wrappee, attr)


class Wrappee(object):
    def bar(self):
        print 'bar'

o2 = Wrappee()
o1 = Wrapper(o2)

o1.foo()
o1.bar()

所有的魔法都发生在Wrapper 类的__getattr__ 方法上,它将尝试访问Wrapper 实例上的方法或属性,如果它不存在,它将尝试包装一。

如果你尝试访问两个类中都不存在的属性,你会得到:

o2.not_valid
Traceback (most recent call last):
  File "so.py", line 26, in <module>
    o2.not_valid
  File "so.py", line 15, in __getattr__
    raise e
AttributeError: 'Wrappee' object has no attribute 'not_valid'

【讨论】:

  • 谢谢,我喜欢这个解决方案。
  • 我还发现这个 (code.activestate.com/recipes/496741-object-proxying) 很好地满足了我的目的。
  • try/except` 除了引发相同的异常之外什么都不做有什么意义?另外,为什么raise e 而不仅仅是raise?根据您的 Python 版本,这可能会丢失异常历史记录或变慢或完全相同,但它无法改进任何东西。
  • @abarnert 你是对的,我把它放在那里是因为它是星期天晚上 11 点,我非常累,而且因为它有效,我并没有在它上面投入太多。我的第一个版本使用 super().__getattr__ 并且调用次数更多,因此不可行。
  • super(Wrapper, self).__getattr__() 不正确(必须将attr 作为参数传递),但偶然在对象上调用__getattr__() 总是会生成AttributeError,因为它不能像那样访问(它是dunder 方法)。主体实际上可以只是return getattr(self.wrappee, attr),因为只有在当前类没有此属性时才会调用该方法,因此无需再次检查它的存在。我将对此进行编辑。
【解决方案2】:

如果你真的需要这个速度很快,最快的选择是在初始化时给自己打补丁:

def __init__(self, wrappee):
    for name, value in inspect.getmembers(wrappee, callable):
        if not hasattr(self, name):
            setattr(self, name, value)

这将为您的Wrapper 实例提供普通数据属性,其值是Wrappee 的绑定方法。那应该非常快。是吗?

class WrapperA(object):
    def __init__(self, wrappee):
        self.wrappee = wrappee
        for name, value in inspect.getmembers(wrappee, callable):
            if not hasattr(self, name):
                setattr(self, name, value)

class WrapperB(object):
    def __init__(self, wrappee):
        self.wrappee = wrappee
    def __getattr__(self, name):
        return getattr(self.wrappee, name)

In [1]: %run wrapper
In [2]: o2 = Wrappee()
In [3]: o1a = WrapperA(o2)
In [4]: o1b = WrapperB(o2)
In [5]: %timeit o2.bar()
10000000 loops, best of 3: 154 ns per loop
In [6]: %timeit o1a.bar()
10000000 loops, best of 3: 159 ns per loop
In [7]: %timeit o1b.bar()
1000000 loops, best of 3: 879 ns per loop
In [8]: %timeit o1b.wrapper.bar()
1000000 loops, best of 3: 220 ns per loop

所以,复制绑定方法有 3% 的成本(不知道为什么它甚至有那么多……)。任何比这更动态的都必须从self.wrapper 中提取属性,这至少有 66% 的开销。通常的__getattr__ 解决方案有 471% 的开销(添加不必要的额外内容只会使其变慢)。

所以,这听起来像是对绑定方法黑客的公开和封闭的胜利,对吧?

不一定。那 471% 的开销仍然只有 700 纳秒。这真的会对您的代码产生影响吗?除非在紧密循环中使用,否则可能不会——在这种情况下,您几乎肯定会想要将方法复制到局部变量中。

而且这种 hack 有很多缺点。这不是“一种明显的方法”。它不适用于未在实例字典上查找的特殊方法。它静态地从o2 中提取属性,因此如果您稍后创建任何新属性,o1 将不会代理它们(尝试以这种方式构建动态代理链......)。如果你有很多代理,它会浪费很多内存。 Python 2.x 和 3.x 之间略有不同(甚至在 2.x 和 3.x 系列中,如果您依赖 inspect),而 __getattr__ 从 2.3 到 2.3 都非常小心地保持不变现在(以及在其他 Python 实现中也是如此)。以此类推。

如果您真的需要速度,您可能需要考虑混合:缓存代理方法的__getattr__ 方法。您甚至可以分两个阶段进行:调用一次,将未绑定的方法缓存在类属性中并动态绑定它;如果它随后被重复调用,则将绑定的方法缓存在实例属性中。

【讨论】:

  • +1 进行比较。听起来您在上一条评论中建议进行某种记忆?
  • @orange:没错。 Memoizing 允许您获得存储绑定方法的大部分速度,而不会失去灵活性或(部分)内存成本(如果您创建对象的频率几乎与调用它们的方法一样频繁,则可能还有启动成本)。但前提是这确实是一个性能热点;正如我所说,它通常不会,当它是时,你通常需要不同的优化(在调用点将绑定的方法复制到本地)。
  • 我大约 90% 确定您未知的 3% 成本将是因为在内部,这些新方法是通过 __dict__ 访问的,而已经存在的方法是使用 __slots__ 访问的,即快一点。
【解决方案3】:

这是另一种猴子补丁方法。这一个直接将方法复制到 Wrapper 类中,而不是创建的包装器对象。此方法的主要优点是所有特殊方法(例如 __add__)都可以使用。

class Wrapper(object):
    def __init__(self, wrappee):
        self.wrappee = wrappee

    def foo(self):
        print('foo')


def proxy_wrap(attr):
    "This method creates a proxy method that calls the wrappee's method."
    def f(self, *args):
        return getattr(self.wrappee, attr)(*args)
    return f

# Don't overwrite any attributes already present
EXCLUDE = set(dir(Wrapper))

# Watch out for this one...
EXCLUDE.add('__class__')

for (attr, value) in inspect.getmembers(Wrappee, callable):
    if attr not in EXCLUDE:
        setattr(Wrapper, attr, proxy_wrap(attr))

我用它来包装 numpy 数组。将Wrappee 设置为np.ndarray

import numpy as np

Wrappee = np.ndarray

# [The block I wrote above]

wrapped = Wrapper(np.arange(10))

wrapped + 1 等操作仍然有效。

【讨论】:

  • 特殊方法的重要性在这种情况下经常被忽视!
【解决方案4】:

对于遇到这个老问题的任何人,他们都在寻找一种开箱即用的解决方案,该解决方案可以处理所有细节,例如类型比较和特殊方法。我遇到了一个声称可以完成所有繁重工作的实用程序包:wrapt

尤其是Object Proxy 部分与原始问题相关:

class CustomProxy(wrapt.ObjectProxy):
    
    def foo(self):
        return 42

wrapper = CustomProxy(wrappee)
wrapper.foo()  # 42
wrapper.bar()  # 12, proxied to wrappee

我遇到的一个缺点:当你想覆盖被包装对象的一个​​属性时,你必须用@property装饰器跳过一些圈,这就是为什么我最终选择了一个非常简单的__getattr__ 覆盖而不是使用wrapt。但是,如果代理在您的应用程序中占很大一部分,那么这可能值得研究。

Wrapt 还有很多用于创建装饰器的实用程序。

免责声明:我实际上并没有在任何代码中使用wrapt,只是在寻找解决方案时遇到它,它似乎符合原始问题的要求,所以我想我会分享。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-09-08
    • 1970-01-01
    • 2012-11-12
    • 2017-03-07
    • 1970-01-01
    • 2012-09-14
    • 1970-01-01
    相关资源
    最近更新 更多