【问题标题】:Is it possible to use `_getattr__`-generated methods in a subclass?是否可以在子类中使用 _getattr__ 生成的方法?
【发布时间】:2023-01-19 02:22:25
【问题描述】:

我有一个类,其方法可能会或可能不会自动生成。我希望能够从子类中调用这些方法,但不知道该怎么做。

考虑这段代码:

class Upgrader:
    max = 99

    def _upgrade_no_op(self, db):
        if self.version < Upgrader.max:
            self.version += 1

    def __getattribute__(self, key):
        try:
            return super().__getattribute__(key)    
        except AttributeError:
            if key.startswith("upgrade_v"):    
                nr = int(key[9:])
                if 0 <= nr < self.max:                   
                    return self._upgrade_no_op
            raise

class Foo(Upgrader):
    version = 1
    def upgrade_v2(self):
        # None of these work:

        # Upgrade.upgrade_v2(self)
        # super().upgrade_v2()
        # super(Foo,self).upgrade_v2()
        # x = Upgrade.__getattribute__(self, "upgrade_v2");  # bound to Foo.upgrade_v2
        breakpoint()

Foo().upgrade_v2()

在我的真实代码中,FooUpgrader 之间还有一两个其他类,但您明白了。

有没有办法做到这一点,最好不要编写我自己的 super() 版本?

【问题讨论】:

  • 只需正常调用该方法即可。 __getattribute__() 方法的继承应使其适用于子类。 self.upgrade_v2()
  • 您是否有理由像现在这样使用__getattribute__(无条件地尝试查看它是否可以在更高级别上找到该方法)?通常,如果您希望在没有您干预的情况下发生可解析的查找,您只需使用__getattr__(仅在找不到属性时调用,就像您的try:/return super().__getattribute__(key)/except AttributeError:一样试图做)。定义 __getattribute__方法更具侵入性(每次查找都会调用它,包括简单的属性,其中 super() 没有意义)。
  • @Barmar:他们称之为upgrade_v2 在子类中,所以如果他们使用self.upgrade_v2(),它就是即时无限递归; self.upgrade_v2()经过Upgrader.__getattribute__,然后得到Foo.upgrade_v2,然后调用self.upgrade_v2(),依此类推,直到它死于一堆upgrade_v2调用(__getattribute__调用可能结束堆栈,但不会在其中看到它,因为它总是在下一次真正调用 Foo.upgrade_v2 发生之前完全解析。
  • 这看起来有点像 XY 问题。什么问题您是否试图通过支持基本上都解析为相同方法的任意命名属性来解决?

标签: python inheritance


【解决方案1】:

你有几个问题:

  1. return super().__getattribute__(key) 在超类(通常是object)上查找__getattribute__,但是key 的实际查找是一个简单的查找,它没有绕过 MRO 中的类,它只是从重新开始查找第一的MRO 中的类(self 实际上是它的一个实例)。 super() 很神奇一次;它将跳过 MRO 中较早的类来执行一次查找,然后就完成了。当属性存在并且从类的方法外部调用你时,这就是你想要的(Foo().update_v2()'s最初的呼叫首先通过Updater.__getattribute__ 找到Foo.update_v2),但是当Foo.update_v2 尝试调用update_v2 的“可能不存在”父版本时,即使您设法调用父@ 987654334@(例如通过直接调用Upgrader.__getattribute__(self, "upgrade_v2")()),它只会再次给你Foo.update_v2,导致无限递归。

  2. super()绕过了很多动态查找机制;在 MRO 中调用它之后,它只会找到每个类都附有适当名称的东西,但它不会尝试在每个类上调用 __getattr____getattribute__ (这会减慢速度显着,并使代码显着复杂化)。依赖__getattribute__ 来提供相当于核心类功能的东西通常已经不是一个好主意了;让它动态拦截全部查找以将事物插入继承链的中间在查找期间充其量只是一些真正强烈的代码气味。

    99.99% 以上的时间,您根本不需要动态查找处理,在极少数情况下,您几乎总是需要 __getattr__(只有在无法以任何其他方式找到名称时才会调用它),而不是__getattribute__(无条件调用)。

    这两种特殊方法都无法按照您想要的方式与 super()-triggered 查找一起使用,所以如果您真的需要像这样的事情,你会被困在重新实现super()手动做的事情,但也添加了动态查找特殊方法支持(手动处理实例的 MRO,从每个类中寻找你想要的东西,然后如果它缺少它,手动调用 __getattr__/__getattribute__ 以查看它是否可以生成它,如果也失败则继续下一节课)。这是疯狂的;不要尝试这样做。

    我强烈怀疑你在这里有an XY problem。你想做的事情本质上是脆弱的,只会让任何在复杂的继承层次结构中,super().upgrade_v2()Foo.upgrade_v2 中实际上可能会在某些类中找到有用的函数,该类与 Foo 一起被一些假设的第三类继承,该第三类涉及返回到 Upgrader 的菱形继承模式.这已经够复杂了,现在你要添加__getattribute__(这会减慢速度每一个使用类实例,即使没有任何类型的继承,它也有很多陷阱);这是个坏主意。

    如果像这种情况一样,您有一组应该存在的固定方法,只需预先生成它们,并完全避免使用 __getattribute__

    class Upgrader:
        max = 99
    
        def _upgrade_no_op(self):
            if self.version < Upgrader.max:
                self.version += 1
    
    # Dynamically bind _upgrade_no_op to each name we intend to support on Upgrader
    for i in range(Upgrader.max):
        setattr(Upgrader, f'upgrade_v{i}', Upgrader._upgrade_no_op)
    
    class Foo(Upgrader):
        version = 1
        def upgrade_v2(self):
            # Upgrade.upgrade_v2(self)
            super().upgrade_v2()  # Works just fine
    
    f = Foo()
    print(f.version)  # See original version of 1 here
    f.upgrade_v2()
    print(f.version)  # See updated version of 2 here
    

    Try it online!

    您的代码将有效,运行速度显着加快(不涉及 Python 级别的函数调用每一个属性和方法查找),它不会驱使维护者疯狂地试图弄清楚你在做什么。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-14
    • 2010-10-28
    • 1970-01-01
    • 1970-01-01
    • 2014-04-30
    • 2016-09-17
    相关资源
    最近更新 更多