【问题标题】:Python class inheritance - spooky actionPython 类继承 - 诡异的动作
【发布时间】:2015-02-27 11:55:37
【问题描述】:

我观察到类继承有一个奇怪的效果。对于我正在进行的项目,我正在创建一个类来充当另一个模块类的包装器。

我正在使用 3rd-party aeidon 模块(用于处理字幕文件),但问题可能不太具体。

这是您通常如何使用该模块...

project = aeidon.Project()
project.open_main(path)

这里是使用中的示例'wrapper'类(当然真实的类有很多方法):

class Wrapper(aeidon.Project):
    pass

project = Wrapper()
project.open_main(path)

上述代码在执行时会引发 AttributeError。但是,以下工作符合我最初的预期:

junk = aeidon.Project()
project = Wrapper()
project.open_main(path)

我以远处的诡异动作命名这个问题,因为我怀疑它涉及环境中的全局变量/对象,但我不知道。

我最终使用组合来解决这个问题(即self.project = aeidon.Project()),但我仍然对此感到好奇。谁能解释这里发生了什么?

这是回溯:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-5-fe548abd7ad0> in <module>()
----> 1 project.open_main(path)

/usr/lib/python3/dist-packages/aeidon/deco.py in wrapper(*args, **kwargs)
    208     def wrapper(*args, **kwargs):
    209         frozen = args[0].freeze_notify()
--> 210         try: return function(*args, **kwargs)
    211         finally: args[0].thaw_notify(frozen)
    212     return wrapper

/usr/lib/python3/dist-packages/aeidon/agents/open.py in open_main(self, path, encoding)
    161         format = aeidon.util.detect_format(path, encoding)
    162         self.main_file = aeidon.files.new(format, path, encoding)
--> 163         subtitles = self._read_file(self.main_file)
    164         self.subtitles, sort_count = self._sort_subtitles(subtitles)
    165         self.set_framerate(self.framerate, register=None)

/usr/lib/python3/dist-packages/aeidon/project.py in __getattr__(self, name)
    116             return self._delegations[name]
    117         except KeyError:
--> 118             raise AttributeError
    119 
    120     def __init__(self, framerate=None, undo_limit=None):

AttributeError:

无论是否调用 Project 的 __init__(),我都试过了。显然这不是在正常情况下应该做的事情,我只是很困惑为什么 Wrapper() 只有在创建垃圾 aeidon.Project() 之后才能按预期运行。

【问题讨论】:

  • 这可能特定于 aeidon 项目,但我们无法确定,因为您没有包含完整的回溯。
  • ipython3 v2.3.1 但我也在 python3 v3.4.0 中测试过

标签: python inheritance multiple-inheritance


【解决方案1】:

aedion.project module 做了两件事:

  • 它将aedion.agents包中的类的方法添加到类中,以便文档生成器在提取文档字符串和其他信息时包含这些方法,使用ProjectMeta metaclass

    class ProjectMeta(type):
    
        """
        Project metaclass with delegated methods added.
        Public methods are added to the class dictionary during :meth:`__new__`
        in order to fool Sphinx (and perhaps other API documentation generators)
        into thinking that the resulting instantiated class actually contains those
        methods, which it does not since the methods are removed during
        :meth:`Project.__init__`.
        """
    

    但是,如果使用这些方法,将无法正确绑定。

  • Project.__init__ 方法调用Project._init_delegations()。此方法删除类中的委托方法:

    # Remove class-level function added by ProjectMeta.
    if hasattr(self.__class__, attr_name):
        delattr(self.__class__, attr_name)
    

    注意这里使用self.__class__。这是必需的,因为如果在类中找到方法,Python 不会通过 __getattr__ 挂钩查找委托方法。

    委托方法绑定到专用代理实例,因此实际上是委托给该代理:

    agent = getattr(aeidon.agents, agent_class_name)(self)
    # ...
    attr_value = getattr(agent, attr_name)
    # ...
    self._delegations[attr_name] = attr_value
    

当您围绕此类创建包装器时,删除 步骤将失败。 self.__class__ 是您的包装器,而不是基础 Project 类。因此方法绑定不正确;正在绑定元类提供的方法,并且永远不会调用 __getattr__ 挂钩来查找委托方法:

>>> import aeidon
>>> class Wrapper(aeidon.Project): pass
... 
>>> wrapper = Wrapper()
>>> wrapper.open_main
<bound method Wrapper.open_main of <__main__.Wrapper object at 0x1106313a8>>
>>> wrapper.open_main.__self__
<__main__.Wrapper object at 0x1106313a8>
>>> wrapper._delegations['open_main']
<bound method OpenAgent.open_main of <aeidon.agents.open.OpenAgent object at 0x11057e780>>
>>> wrapper._delegations['open_main'].__self__
<aeidon.agents.open.OpenAgent object at 0x11057e780>

因为Project 上的open_main 方法仍然存在:

>>> Project.open_main
<function OpenAgent.open_main at 0x110602bf8>

一旦您创建了Project 的实例,这些方法就会从类中删除:

>>> Project()
<aeidon.project.Project object at 0x1106317c8>
>>> Project.open_main
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Project' has no attribute 'open_main'

您的包装器将开始工作,因为现在找到了委派的open_main

>>> wrapper.open_main
<bound method OpenAgent.open_main of <aeidon.agents.open.OpenAgent object at 0x11057e780>>
>>> wrapper.open_main.__self__
<aeidon.agents.open.OpenAgent object at 0x11057e780>

您的包装器必须自己进行删除:

class Wrapper(aeidon.Project):
    def __init__(self):
        super().__init__()
        for name in self._delegations:
            if hasattr(aeidon.Project, name):
                delattr(aeidon.Project, name)

请注意,如果 aedion 维护人员仅将 self.__class__ 替换为 __class__(没有 self),他们的代码仍然可以工作并且您的子类方法也可以工作,而无需手动执行再次进行班级大扫除。这是因为在 Python 3 中,__class__ 引用 in methods 是一个自动闭包变量,指向定义了方法的类。对于Project._init_delegations(),那就是Project。或许您可以提交一份错误报告。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-03-10
    • 2016-05-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多