Python 类继承——诡异的动作 [英] Python class inheritance - spooky action

查看:73
本文介绍了Python 类继承——诡异的动作的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

以下是您通常使用该模块的方式...

project = aeidon.Project()project.open_main(路径)

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

class Wrapper(aeidon.Project):经过项目 = 包装()project.open_main(路径)

上述代码在执行时会引发 AttributeError.但是,以下工作正如我最初预期的那样:

junk = aeidon.Project()项目 = 包装()project.open_main(路径)

我以远距离幽灵般的动作命名这个问题,因为我怀疑它涉及环境中的全局变量/对象,但我不知道.

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

这是回溯:

---------------------------------------------------------------------------AttributeError 回溯(最近一次调用最后一次)<ipython-input-5-fe548abd7ad0>在 <module>()---->1 project.open_main(path)包装器中的/usr/lib/python3/dist-packages/aeidon/deco.py(*args, **kwargs)208 定义包装器(*args,**kwargs):209 冻结 = args[0].freeze_notify()-->210 尝试:返回函数(*args,**kwargs)211 最后:args[0].thaw_notify(frozen)212 返回包装器/usr/lib/python3/dist-packages/aeidon/agents/open.py in open_main(self, path, encoding)161格式= aeidon.util.detect_format(路径,编码)162 self.main_file = aeidon.files.new(格式,路径,编码)-->163 字幕 = self._read_file(self.main_file)164 self.subtitles, sort_count = self._sort_subtitles(subtitles)165 self.set_framerate(self.framerate,寄存器=无)/usr/lib/python3/dist-packages/aeidon/project.py 在 __getattr__(self, name)第116话117 除了 KeyError:-->118 引发属性错误119120 def __init__(self, framerate=None, undo_limit=None):属性错误:

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

解决方案

aedion.project 模块 做了两件事:

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

    class ProjectMeta(type):"""添加了委托方法的项目元类.在 :meth:`__new__` 期间将公共方法添加到类字典中为了愚弄 Sphinx(或许还有其他 API 文档生成器)认为生成的实例化类实际上包含那些方法,它不会因为这些方法在:meth:`Project.__init__`."""

    如果使用这些方法,将无法正确绑定.

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

    # 移除 ProjectMeta 添加的类级函数.如果 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__ 钩子永远不会被调用来查找委托的方法:

<预><代码>>>>进口爱登>>>类包装器(aeidon.Project):通过...>>>包装器 = 包装器()>>>包装器.open_main<0x1106313a8处<__main__.Wrapper对象的绑定方法Wrapper.open_main>>>>>wrapper.open_main.__self__<__main__.Wrapper 对象在 0x1106313a8>>>>wrapper._delegations['open_main']<0x11057e780处<aeidon.agents.open.OpenAgent对象的绑定方法OpenAgent.open_main>>>>>wrapper._delegations['open_main'].__self__<aeidon.agents.open.OpenAgent 对象在 0x11057e780>

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

<预><代码>>>>项目.open_main<函数 OpenAgent.open_main 在 0x110602bf8>

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

<预><代码>>>>项目()<aeidon.project.Project 对象在 0x1106317c8>>>>项目.open_main回溯(最近一次调用最后一次):文件<stdin>",第 1 行,在 <module> 中AttributeError: 类型对象Project"没有属性open_main"

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

<预><代码>>>>包装器.open_main<0x11057e780处<aeidon.agents.open.OpenAgent对象的绑定方法OpenAgent.open_main>>>>>wrapper.open_main.__self__<aeidon.agents.open.OpenAgent 对象在 0x11057e780>

您的包装器必须自己执行删除操作:

class Wrapper(aeidon.Project):def __init__(self):super().__init__()对于 self._delegations 中的名称:如果 hasattr(aeidon.Project, name):delattr(aeidon.Project,名称)

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

I've observed a strange effect with class inheritance. For the project I'm working on, I'm creating a class to act as a wrapper to another module's class.

I am using the 3rd-party aeidon module (used for manipulating subtitle files) but the issue is probably less specific.

Here is how you'd normally utilize the module...

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

Here is the example 'wrapper' class in use (of course the real class has many methods):

class Wrapper(aeidon.Project):
    pass

project = Wrapper()
project.open_main(path)

This aforementioned code raises an AttributeError upon execution. However, the following works as I originally expected it to:

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

I named this question after spooky action at a distance because I suspect that it involves global vars/objects in the environment, but I don't know.

I ended up using composition to solve this problem (i.e. self.project = aeidon.Project()) but I'm still curious about this. Can anyone explain what's going on here?

Here is the traceback:

---------------------------------------------------------------------------
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:

I've tried it both with and without a call to Project's __init__(). Obviously this is not really something that should be done under normal circumstances, I'm just perplexed why Wrapper() would function as expected only after creating a junk aeidon.Project().

解决方案

The aedion.project module does two things:

  • It adds methods from classes in the aedion.agents package to the class, in order for the documentation generator to include these when extracting docstrings and other information, using a 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__`.
        """
    

    these methods, if used, would not be correctly bound however.

  • The Project.__init__ method calls Project._init_delegations(). This method deletes the delegated methods from the class:

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

    Note the use of self.__class__ here. This is needed because Python won't look for the delegated method via the __getattr__ hook if the method is found on the class instead.

    The delegated methods are bound to a dedicated agent instance, so are in fact delegating to that agent:

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

When you create a wrapper around this class, the deletion step fails. self.__class__ is your wrapper, not the base Project class. Thus the methods are bound incorrectly; the metaclass-provided methods are being bound, and the __getattr__ hook is never invoked to find the delegated methods instead:

>>> 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>

because the open_main method on Project still exists:

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

As soon as you create an instance of Project those methods are deleted from the class:

>>> 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'

and your wrapper would start to work as now the delegated open_main is found:

>>> 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>

Your wrapper will have to do the deletions itself:

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

Note that if the aedion maintainers replaced self.__class__ with just __class__ (no self) their code would still work and your subclass approach would also work without having to manually do the class clean-up again. That's because in Python 3, the __class__ reference in methods is an automatic closure variable pointing to the class on which a method was defined. For Project._init_delegations() that'd be Project. Perhaps you could file a bug report to that effect.

这篇关于Python 类继承——诡异的动作的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆