35

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

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

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

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

4

8 回答 8

30

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

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

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

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

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

于 2012-11-08T00:46:12.683 回答
17

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

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

于 2012-11-08T01:28:04.567 回答
5

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

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

于 2012-11-08T01:14:25.050 回答
3

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

首先,我查找这篇文章的原因是因为我只是这样做并且__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) 语法。

于 2014-06-28T04:42:32.003 回答
1

其他答案很好地讨论了为什么仅仅改变__class__可能不是最佳决定的问题。

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

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)
于 2018-05-15T21:37:14.657 回答
0

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

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

于 2012-11-08T00:46:03.857 回答
-1

这是一个示例,您可以在不更改__class__. 在对问题的评论中引用@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,但这是一个完全正常和 pythonic 的事情,不会混淆任何人。

另外,它允许您更改某些您不想更改的特殊方法,只需在Cell.

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

如果您拒绝更改__class__,那么您可能必须包含一个阶段属性,并使用大量 if 语句,或者重新分配许多指向不同阶段功能的属性

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

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

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

于 2012-11-08T01:15:07.467 回答
-1

在评论中,我建议将元胞自动机建模为动态__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__,没有额外的间接级别)

于 2012-11-08T02:40:13.007 回答