【问题标题】:Intercept magic method calls in python class拦截python类中的魔术方法调用
【发布时间】:2020-03-10 14:51:33
【问题描述】:

我正在尝试创建一个包含将在多个其他对象中使用的值的类。出于计算的原因,目的是让这个包装的值只计算一次,并将对该值的引用传递给它的用户。由于它的对象容器模型,我不相信这在 vanilla python 中是可能的。相反,我的方法是一个传递的包装类,定义如下:

class DynamicProperty():

    def __init__(self, value = None):
        # Value of the property
        self.value: Any = value

    def __repr__(self):
        # Use value's repr instead
        return repr(self.value)

    def __getattr__(self, attr):
        # Doesn't exist in wrapper, get it from the value 
        # instead
        return getattr(self.value, attr)

以下按预期工作:

wrappedString = DynamicProperty("foo")
wrappedString.upper()  # 'FOO'

wrappedFloat = DynamicProperty(1.5)
wrappedFloat.__add__(2)  # 3.5

但是,通过正常语法隐式调用__add__ 失败:

wrappedFloat + 2  # TypeError: unsupported operand type(s) for 
                  # +: 'DynamicProperty' and 'float'

有没有办法拦截这些隐式方法调用,而无需为 DynamicProperty 显式定义魔术方法来调用其value 属性上的方法?

【问题讨论】:

    标签: python class proxy


    【解决方案1】:

    谈论“通过引用传递”只会让您感到困惑。将该术语保留在您可以选择的语言中,并且它会有所作为。在 Python 中,您总是传递 objects - 这种传递相当于“通过引用传递” - 对于 所有 对象 - 从 None 到 int 到实时异步网络连接池实例。

    顺便说一句:语言从对象中检索属性所遵循的算法很复杂,有细节 - 实现__getattr__ 只是冰山一角。完整阅读名为“Data Model”的文档将使您更好地掌握检索属性所涉及的所有机制。

    也就是说,这就是“魔术”或“dunder”方法的工作原理 - (名称前有两个下划线和名称后有两个下划线的特殊函数):当您使用需要存在实现它的方法的运算符时(如__add__ 用于+),该语言会检查对象的类 中的__add__ 方法——而不是实例。并且类上的__getattr__ 只能为该类的实例动态创建属性。 但这不是唯一的问题:您可以创建一个元类(继承自 type)并在此元类上放置一个 __getattr__ 方法。对于您将从 Python 执行的所有查询,看起来您的对象在其类中具有 __add__(或任何其他 dunder 方法)。但是,对于 dunder 方法,Python 不会通过正常的属性查找机制——如果 dunder 方法“物理地”存在于那里,它会直接“查看”类。内存结构中有一些槽,用于保存每个可能的 dunder 方法的类 - 它们要么引用相应的方法,要么为“null”(在 Python 端用 C 编码时,这是“可见的”,默认dir 将在这些方法存在时显示它们,如果不存在则省略它们)。如果它们不存在,Python 只会“说”该对象没有实现该操作和周期。

    使用您想要的代理对象解决该问题的方法是创建一个代理类,该代理类具有您要包装的类中的 dunder 方法,或具有所有可能的方法,并在被调用时检查是否底层对象实际上实现了被调用的方法。

    这就是为什么“严肃”的代码很少(如果有的话)提供真正的“透明”代理对象。有例外,但从“Weakrefs”到“super()”,再到 concurrent.futures,仅在核心语言和 stdlib 中提及一些,没有人尝试“完全工作的透明代理”——相反,api 是更像是在包装器上调用“.value()”或“.result()”方法来获取原始对象本身。

    但是,它可以完成,就像我上面描述的那样。我什至在 pypi 上有一个小的(长期未维护的)包可以做到这一点,为未来包装一个代理。 代码在https://bitbucket.org/jsbueno/lelo/src/master/lelo/_lelo.py

    【讨论】:

    • 感谢您的详细解释,这正是我正在寻找的信息。我会采纳您的建议,避免使用代理对象。
    【解决方案2】:

    您的情况下的+ 运算符不起作用,因为DynamicProperty 不继承自float。见:

    >>> class Foo(float):
        pass
    
    >>> Foo(1.5) + 2
    3.5
    

    因此,您需要进行某种动态继承:

    def get_dynamic_property(instance):
    
        base = type(instance)
    
        class DynamicProperty(base):
            pass
    
        return DynamicProperty(instance)
    
    
    wrapped_string = get_dynamic_property("foo")
    print(wrapped_string.upper())
    
    wrapped_float = get_dynamic_property(1.5)
    print(wrapped_float + 2)
    

    输出:

    FOO
    3.5
    

    【讨论】:

    • 虽然你可以继承任何你想包装的东西,但它并不一定允许你通过引用传递。例如,我无法创建 class FloatWrapper(int): ... 的实例、传递其引用并更改实例值,以使所有原始引用现在都指向更新后的值。
    猜你喜欢
    • 2012-02-21
    • 2012-04-01
    • 2011-02-11
    • 1970-01-01
    • 2015-08-08
    • 1970-01-01
    • 2014-10-11
    相关资源
    最近更新 更多