【问题标题】:How dangerous is setting self.__class__ to something else?将 self.__class__ 设置为其他东西有多危险?
【发布时间】:2012-10-28 03:44:36
【问题描述】:

假设我有一个类,它有许多子类。

我可以实例化这个类。然后我可以将其__class__ 属性设置为子类之一。我已经有效地将类类型更改为其子类的类型,在一个活动对象上。我可以调用它的方法来调用这些方法的子类版本。

那么,这样做有多危险?看起来很奇怪,但这样做是不是错误?尽管能够在运行时更改类型,但这是应该完全避免的语言特性吗?为什么或为什么不?

(根据回复,我将发布一个更具体的问题,说明我想做什么,以及是否有更好的选择)。

【问题讨论】:

  • 我会说这是错误的,主要是因为没有客户端代码会期望对象的类型可以随机更改。当然,如果您将类型更改为与前一种兼容的类型,则不会引入错误。但是可能有更好的面向对象的方法。 (委派策略等)所以我的直觉是:危险?不,令人困惑?是的。有用到足以引起混乱吗?不太可能。
  • 我们通常认为一个实例有固定的方法和可变的数据。我很想看到一个具有固定数据和可变方法的用例。我觉得会很有趣!请向我们展示您的用例。
  • 使用工厂设计模式的设计看起来很糟糕......
  • 请发表具体问题。很难想象这将是最佳解决方案的情况——但仅仅因为我无法想象一个并不意味着它不存在。 :)
  • @unutbu 函数可以是数据,因此“变量方法”更适合通过保存函数对象的属性来建模。

标签: python python-3.x python-2.x


【解决方案1】:

以下是我能想到的导致这种危险的事情的清单,按从最坏到最不坏的粗略顺序排列:

  • 阅读或调试您的代码的人可能会感到困惑。
  • 您不会得到正确的__init__ 方法,因此您可能不会正确初始化所有实例变量(甚至根本不会)。
  • 2.x 和 3.x 之间的差异非常显着,移植起来可能会很痛苦。
  • 存在一些带有类方法、手动编码描述符、方法解析顺序挂钩等的极端情况,它们在经典类和新型类之间是不同的(同样,在 2.x 和 3.x 之间)。 x)。
  • 如果您使用__slots__,则所有类必须具有相同的插槽。 (如果你有兼容但不同的插槽,它可能一开始似乎可以工作,但会做一些可怕的事情......)
  • 新型类中的特殊方法定义可能不会改变。 (事实上​​,这在实践中适用于所有当前的 Python 实现,但它没有记录可以工作,所以……)
  • 如果您使用__new__,事情将不会按照您天真预期的方式进行。
  • 如果类有不同的元类,事情会变得更加混乱。

同时,在您认为有必要这样做的许多情况下,还有更好的选择:

  • 使用工厂动态创建适当类的实例,而不是创建基实例,然后将其混入派生实例。
  • 使用__new__ 或其他机制来挂钩构造。
  • 重新设计事物,使您拥有一个具有某些数据驱动行为的类,而不是滥用继承。

作为最后一个最常见的特定情况,只需将所有“变量方法”放入其实例作为“父”的数据成员保存的类中,而不是放入子类中。不要更改self.__class__ = OtherSubclass,只需更改self.member = OtherSubclass(self)。如果你真的需要神奇地改变方法,那么自动转发(例如,通过__getattr__)是一种比动态更改类更常见和 Python 的习惯用法。

【讨论】:

  • 能否详细说明 2-3 移植问题?
  • @agf:例如,我有一个名为sm@staticmethod。我做m = myObj.sm; myObj.__class__ = Derived; print(m == myObj.sm)。在 2.x 中,这是真的;在 3.x 中,它是 False。
  • 与插槽问题有关,特别是如果两个类之间的存储结构不兼容(对于插槽或其他),分配给__class__会失败。这会影响listtuple 之类的东西,您不能在子类中使用__slots__
  • @TokenMacGuy:感谢您的澄清。这就是我所说的“不兼容”案例。但是你至少会得到一个很好的错误(比如TypeError: __class__ assignment: 'B' object layout differs from 'A')。如果你有两个具有兼容插槽的类,它们持有不同的描述符类型(你可能不应该这样做,但是如果你是那种重新分配__class__的人...... ),它可能不太清楚。
  • @abarnert,谢谢你的详细回答。对我来说最有说服力的因素是潜在的未来不兼容性,以及它实际上没有记录到能够做到这一点,因此可能不是一个好的前进道路。我以另一种方式解决了 init 问题;不使用插槽;不使用元类。这正是我正在寻找的答案。谢谢。
【解决方案2】:

如果您的应用程序运行时间很长,并且您需要将某个对象的旧版本替换为同一类的较新版本而不丢失数据,则分配 __class__ 属性非常有用,例如经过一些reload(mymodule) 并且没有重新加载未更改的模块。另一个例子是如果你实现持久性 - 类似于pickle.load

不鼓励所有其他用法,特别是如果您可以在启动应用程序之前编写完整的代码。

【讨论】:

  • +1 用于合理使用分配__class__。至少这应该会阻止有人问,“但如果这很糟糕,为什么 Python 让我这样做呢?”
【解决方案3】:

在任意类上,这极不可能起作用,即使它起作用也非常脆弱。这与将底层函数对象从一个类的方法中提取出来,然后在不是原始类实例的对象上调用它们基本上是一回事。这是否可行取决于内部实现细节,并且是一种非常紧密耦合的形式。

也就是说,在一组特别设计以这种方式使用的类中更改对象的__class__ 可能完全没问题。很长一段时间以来,我都知道您可以这样做,但我还没有发现这种技术的用途,即没有同时想到更好的解决方案。因此,如果您认为自己有一个用例,那就去做吧。请在您的 cmets/文档中明确发生了什么。特别是这意味着所有涉及的类的实现必须尊重它们的不变量/假设/等所有,而不是能够孤立地考虑每个类,所以你要确保任何处理相关代码的人都知道这一点!

【讨论】:

  • 不,它实际上很有可能奏效。大多数类没有__slots__、自定义metaclasses、@staticmethods 或其他可能/将破坏您的代码的东西。它更有可能会工作,但 6 个月后它会因为一些看似无关的更改而崩溃,或者只是让维护你的代码的人混淆......
  • @abarnert 我不了解你,但是当我编写 MyFancyClass 的代码实例时,不太可能具有 MyCompletelyUnrelatedClass 的方法所期望的属性。 __slots__、元类等甚至都没有进入它(尽管据我所知,元类和静态方法尊重对__class__ 的更改就好了)。 SomeClasseInAHeirarchySomeOtherClassInAHeirarchy 的实例更有可能应对在它们之间切换 __class__,但仍然不太可能。
  • 是的,但是 OP 不是在谈论 MyCompletelyUnrelatedClass,而是在谈论 MyTightlyCoupledSubclass
  • @abarnert 是的,但是(a)当我说“这极不可能工作”时,它是在“任意课程”之后立即出现的,并且(b)它仍然不是同一类层次结构的两个紧密耦合的成员根本不可能具有不同的属性。更改 __class__ 不是“很可能会起作用”,除非您已明确设计它可以正常工作。
【解决方案4】:

好吧,不要忽视一开始就警告过的问题。但在某些情况下它可能很有用。

首先,我查找这篇文章的原因是因为我这样做了,而__slots__ 不喜欢它。 (是的,我的代码是插槽的有效用例,这是纯粹的内存优化)并且我试图解决插槽问题。

我第一次看到这个是在 Alex Martelli 的 Python Cookbook(第 1 版)中。在第 3 版中,它是 8.19 节“实现有状态对象或状态机问题”。一个知识渊博的来源,Python 方面。

假设您有一个 ActiveEnemy 对象,该对象的行为与 InactiveEnemy 不同,您需要在它们之间快速来回切换。甚至可能是死敌。

如果 InactiveEnemy 是子类或同级,您可以切换类属性。更准确地说,确切的祖先比与调用它的代码一致的方法和属性更重要。想想 Java 接口,或者,正如一些人所提到的,您的类需要在设计时考虑到这种用途。

现在,您仍然需要管理状态转换规则和各种其他事情。而且,是的,如果您的客户端代码没有预料到这种行为并且您的实例切换了行为,那么事情就会大发雷霆。

但我已经在 Python 2.x 上非常成功地使用了它,并且从未遇到任何异常问题。最好在具有相同方法签名的子类上使用共同的父类和小的行为差异。

没问题,直到我的 __slots__ 问题刚刚阻止它。但是插槽一般来说是一件令人头疼的事情。

我不会这样做来修补实时代码。我还希望使用工厂方法来创建实例。

但是要管理预先知道的非常具体的条件?就像期望客户彻底理解的状态机一样?然后它非常接近魔法,伴随着所有的风险。很优雅。

Python 3 问题?测试它是否有效,但 Cookbook 在其示例 FWIW 中使用 Python 3 print(x) 语法。

【讨论】:

    【解决方案5】:

    其他答案很好地讨论了为什么只更改 __class__ 可能不是最佳决定。

    下面是使用__new__ 在实例创建后避免更改__class__ 的一个示例。为了完整起见,我不推荐它,只是展示它可以如何完成。然而,最好使用一个无聊的旧工厂来做这件事,而不是将鞋拔继承到一个原本不打算做的工作中。

    class ChildDispatcher:
        _subclasses = dict()
        def __new__(cls, *args, dispatch_arg, **kwargs):
            # dispatch to a registered child class
            subcls = cls.getsubcls(dispatch_arg)
            return super(ChildDispatcher, subcls).__new__(subcls)
        def __init_subclass__(subcls, **kwargs):
            super(ChildDispatcher, subcls).__init_subclass__(**kwargs)
            # add __new__ contructor to child class based on default first dispatch argument
            def __new__(cls, *args, dispatch_arg = subcls.__qualname__, **kwargs):
                return super(ChildDispatcher,cls).__new__(cls, *args, **kwargs)
            subcls.__new__ = __new__
            ChildDispatcher.register_subclass(subcls)
    
        @classmethod
        def getsubcls(cls, key):
            name = cls.__qualname__
            if cls is not ChildDispatcher:
                raise AttributeError(f"type object {name!r} has no attribute 'getsubcls'")
            try:
                return ChildDispatcher._subclasses[key]
            except KeyError:
                raise KeyError(f"No child class key {key!r} in the "
                               f"{cls.__qualname__} subclasses registry")
    
        @classmethod
        def register_subclass(cls, subcls):
            name = subcls.__qualname__
            if cls is not ChildDispatcher:
                raise AttributeError(f"type object {name!r} has no attribute "
                                     f"'register_subclass'")
            if name not in ChildDispatcher._subclasses:
                ChildDispatcher._subclasses[name] = subcls
            else:
                raise KeyError(f"{name} subclass already exists")
    
    class Child(ChildDispatcher): pass
    
    c1 = ChildDispatcher(dispatch_arg = "Child")
    assert isinstance(c1, Child)
    c2 = Child()
    assert isinstance(c2, Child)
    

    【讨论】:

      【解决方案6】:

      它有多“危险”主要取决于子类在初始化对象时会做什么。它完全有可能无法正确初始化,只运行基类的__init__(),并且稍后会因为未初始化的实例属性而失败。

      即使没有,对于大多数用例来说,这似乎也是一种不好的做法。首先实例化所需的类更容易。

      【讨论】:

        【解决方案7】:

        这是一个示例,说明您可以在不更改 __class__ 的情况下做同样的事情。在 cmets 中引用@unutbu 来回答问题:

        假设您正在建模元胞自动机。假设每个单元格可能处于 5 个阶段之一。您可以定义 5 个类 Stage1、Stage2 等。假设每个 Stage 类都有多个方法。

        class Stage1(object):
          …
        
        class Stage2(object):
          …
        
        …
        
        class Cell(object):
          def __init__(self):
            self.current_stage = Stage1()
          def goToStage2(self):
            self.current_stage = Stage2()
          def __getattr__(self, attr):
            return getattr(self.current_stage, attr)
        

        如果您允许更改__class__,您可以立即为单元格提供新阶段的所有方法(名称相同,但行为不同)。

        更改current_stage 也是如此,但这是一件非常正常且符合 Python 的事情,不会让任何人感到困惑。

        另外,它允许您更改某些您不想更改的特殊方法,只需在 Cell 中覆盖它们即可。

        另外,它适用于数据成员、类方法、静态方法等,每个中级 Python 程序员都已经理解。

        如果你拒绝更改__class__,那么你可能不得不包含一个stage属性,并使用大量if语句,或者重新分配大量指向不同stage函数的属性

        是的,我使用了阶段属性,但这不是缺点——它是跟踪当前阶段是什么的明显可见的方式,更利于调试和可读性。

        除了 stage 属性之外,没有一个 if 语句或任何属性重新分配。

        这只是在不更改 __class__ 的情况下执行此操作的多种不同方式之一。

        【讨论】:

          【解决方案8】:

          在 cmets 中,我建议将元胞自动机建模作为动态 __class__s 的一个可能用例。让我们试着充实一下这个想法:


          使用动态__class__

          class Stage(object):
              def __init__(self, x, y):
                  self.x = x
                  self.y = y
          
          class Stage1(Stage):
              def step(self):
                  if ...:
                      self.__class__ = Stage2
          
          class Stage2(Stage):
              def step(self):
                  if ...:
                      self.__class__ = Stage3
          
          cells = [Stage1(x,y) for x in range(rows) for y in range(cols)]
          
          def step(cells):
              for cell in cells:
                  cell.step()
              yield cells
          

          由于没有更好的术语,我将称之为

          传统方式:(主要是abarnert的代码)

          class Stage1(object):
              def step(self, cell):
                  ...
                  if ...:
                      cell.goToStage2()
          
          class Stage2(object):
              def step(self, cell):
                  ...
                  if ...:        
                      cell.goToStage3()
          
          class Cell(object):
              def __init__(self, x, y):
                  self.x = x
                  self.y = y
                  self.current_stage = Stage1()
              def goToStage2(self):
                  self.current_stage = Stage2()
              def __getattr__(self, attr):
                  return getattr(self.current_stage, attr)
          
          cells = [Cell(x,y) for x in range(rows) for y in range(cols)]
          
          def step(cells):
              for cell in cells:
                  cell.step(cell)
              yield cells
          

          比较:

          • 传统方式创建一个Cell 实例列表,每个实例都带有一个 当前阶段属性。

            动态__class__ 方式创建一个实例列表,这些实例是 Stage 的子类。不需要当前阶段 属性,因为__class__ 已经用于此目的。

          • 传统方式使用goToStage2,goToStage3,...方法 切换阶段。

            动态__class__ 方式不需要这样的方法。你刚才 重新分配__class__

          • 传统方式使用特殊方法__getattr__进行委托 对保存在 self.current_stage 属性。

            动态__class__ 方式不需要任何此类委托。这 cells 中的实例已经是你想要的对象了。

          • 传统方式需要将cell作为参数传递给 Stage.step。这样cell.goToStageN 就可以被调用了。

            动态__class__ 方式不需要传递任何东西。这 我们正在处理的对象拥有我们需要的一切。


          结论:

          这两种方式都可以发挥作用。就我可以想象这两种实现将如何发展而言,在我看来动态__class__ 实现将是

          • 更简单(没有Cell 类),

          • 更优雅(没有丑陋的goToStage2 方法,没有像为什么这样的脑筋急转弯 你需要写cell.step(cell)而不是cell.step()),

          • 并且更容易理解(没有__getattr__,没有额外的 间接)

          【讨论】:

          • 你的比较是愚蠢的。例如,“传统方式使用goToStage2,goToStage3,...方法切换阶段。动态__class__方式不需要这样的方法。你只需重新分配__class__。”传统方式也不需要这样的方法;它们是微不足道的单行代码,我只是将它们包装在函数中以为它们命名并使示例更具可读性。此外,您不需要将cell 作为参数传递给每个方法;您将其存储为舞台的成员。 “没有Cell 类”只是因为您将Cell 类重命名为Stage。以此类推。
          • 如果你愿意,我可以编辑你答案的“传统”部分,以显示它应该如何实际编写,然后你可以再次尝试比较,而不是与一个糟糕的结构进行比较稻草人。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2016-09-17
          • 2020-10-18
          • 2016-07-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多