在Python中调用每个祖先的方法

2 投票
3 回答
606 浏览
提问于 2025-04-15 17:32

我在Python中有一个'D'类的对象,我想依次执行'D'及其祖先类('A'、'B'和'C')定义的'run'方法。

我可以这样做到:

class A(object):
    def run_all(self):
        # I prefer to execute in revere MRO order
        for cls in reversed(self.__class__.__mro__):
            if hasattr(cls, 'run'):
                # This works
                cls.run(self)
                # This doesn't
                #cls.__getattribute__(self, 'run')()

    def run(self):
        print "Running A"

class B(A):
    def run(self):
        print "Running B"

class C(A):
    def run(self):
        print "Running C"

class D(C, B):
    def run(self):
        print "Running D"

if __name__ == "__main__":
    D().run_all()

这样做的结果是:

$ python test.py 
Running A
Running B
Running C
Running D

不过实际上,我并不知道要执行的方法的名字。但是如果我尝试使用getattribute()(见注释的那行),它就不管用了:

$ python test.py 
Running D
Running D
Running D
Running D

所以我有几个问题:

  1. 为什么这样不行呢?

  2. 这样做真的是最好的方法吗?

3 个回答

1

你为什么不直接用 super 呢?虽然有些人认为它有点问题,但它的设计就是为了应对这种情况,所以我会毫不犹豫地使用它。

根据 Python 的文档

这个功能很有用,可以用来访问在类中被重写的继承方法。它的搜索顺序和 getattr() 一样,只是跳过了类型本身。 [...] 这使得实现“钻石图”成为可能,也就是多个基类实现同一个方法的情况。

更新:在你的情况下,它会变成这样:

class A(object):

    def run(self):
        print "Running A"

class B(A):
    def run(self):
        super(B, self).run()
        print "Running B"

class C(A):
    def run(self):
        super(C, self).run()
        print "Running C"

class D(C, B):
    def run(self):
        super(D, self).run()
        print "Running D"

if __name__ == "__main__":
    D().run()
1

你不应该使用 __getattribute__ 这个方法。

你只需要这样做:

getattr(cls, 'run')(self)
4

如果你愿意修改所有的 run 实现(并在 D 中调用 run 而不是 run_all),这样做是可行的:

class A(object):
    def run(self):
        print "Running A"

class B(A):
    def run(self):
        super(B, self).run()
        print "Running B"

class C(A):
    def run(self):
        super(C, self).run()
        print "Running C"

class D(C, B):
    def run(self):
        super(D, self).run()
        print "Running D"

if __name__ == "__main__":
    D().run()

注意,我在根类中没有使用 super -- 它“知道”没有更高的父类可以调用(object 并没有定义 run 方法)。不幸的是,在 Python 2 中,这样做会显得有点啰嗦(而且不太适合用装饰器来实现)。

你对 hasattr 的检查有点脆弱,如果我理解你的意图没错的话 -- 如果一个类定义了或继承了某个属性,它就会被认为“拥有”这个属性。所以如果你有一个中间类,它没有重写 run,但在 __mro__ 中出现,那么在你的方法中,它继承的 run 版本会被调用两次。例如,考虑:

class A(object):
    def run_all(self):
        for cls in reversed(self.__class__.__mro__):
            if hasattr(cls, 'run'):
                getattr(cls, 'run')(self)
    def run(self):
        print "Running A"
class B(A): pass
class C(A):
    def run(self):
        print "Running C"
class D(C, B): pass

if __name__ == "__main__":
    D().run_all()

这会打印

Running A
Running A
Running C
Running C

对于 BD 继承但没有重写的 run 版本,会出现两个“重复”。假设我说的没错,这并不是你想要的效果,如果你想避免使用 super,你可以尝试将 run_all 改成:

def run_all(self):
    for cls in reversed(self.__class__.__mro__):
        meth = cls.__dict__.get('run')
        if meth is not None: meth(self)

这样替换到我最新的例子中,只需在 AC 中有两个不同的 def 定义 run,就会使例子打印:

Running A
Running C

我怀疑这可能更接近你想要的结果。

还有一点:不要重复工作 -- 使用 hasattr 来保护 getattr,或者用 in 测试来保护字典访问 -- 保护中的检查和被保护的访问器,内部必须重复完全相同的工作,这样做没有任何好处。相反,使用 getattr 调用的第三个参数 None(或者字典的 get 方法):这意味着如果方法不存在,你会得到一个 None 值,然后你可以针对这种情况来保护 调用。这正是字典有 get 方法和 getattr 有第三个可选“默认”参数的原因:为了方便应用 DRY 原则,“不要重复自己”,这是良好编程中非常重要的一条原则!-)

撰写回答