将自我设置为危险



说我有一个类,有许多子类。

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

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

(根据答复,我会发布一个更明确的问题,内容是我想做什么,如果有更好的选择)。

这是我能想到的事情的清单,使这种危险的命令从最糟糕的到最低糟糕:

  • 可能会使某人阅读或调试您的代码。
  • 您不会获得正确的__init__方法,因此您可能不会正确初始化所有实例变量(甚至根本不正确)。
  • 2.x和3.x之间的差异足够显着,以至于端口可能很痛苦。
  • 有一些边缘案例,带有分类,手工编码的描述符,方法分辨率顺序等,它们之间的经典和新样式类别(同样在2.x和3之间)。x)。
  • 如果您使用__slots__,则所有类都必须具有相同的插槽。(如果您具有兼容但不同的插槽,则可能起初可能起作用,但要做可怕的事情……)
  • 新样式类中的特殊方法定义可能不会改变。(实际上,这将在实践中与所有当前的Python实施一起工作,但没有记录在的工作,所以…)
  • 如果您使用__new__,那么事情就不会像您天真的预期一样工作。
  • 如果类具有不同的元素,情况将变得更加令人困惑。

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

  • 使用工厂动态创建适当类的实例,而不是创建基本实例,然后将其刺入派生的实例。
  • 使用__new__或其他机制连接结构。
  • 重新设计事物,因此您有一个具有某些数据驱动行为的单个类,而不是滥用继承。

作为最后一个情况的最常见情况,只需将所有"变量方法"放入其实例作为"父"的数据成员而不是子类中的类别的类别。而不是更改self.__class__ = OtherSubclass,只需执行self.member = OtherSubclass(self)即可。如果您确实需要神奇地更改的方法,那么自动转发(例如,通过__getattr__)比飞行更改类别更为普遍,Pythonic Sidiom。>分配__class__属性如果您有长时间的运行应用程序,并且需要通过同一类的较新版本替换某些对象的旧版本,而不会丢失数据,例如经过一些reload(mymodule)之后,没有重新加载未改变的模块。另一个示例是,如果您实现了持久性 - 类似于pickle.load

所有其他用法都是不建议的,尤其是如果您在启动应用程序之前可以编写完整的代码。

在任意类中,这极不可能工作,即使这样做也很脆弱。基本上,这与将基础函数对象从一个类的方法中删除,并在不是原始类实例的对象上调用它们。这是否有效取决于内部实施细节,并且是一种非常紧密的耦合的形式。

也就是说,在一组的类中更改对象的__class__,特别是设计可以使用这种方式。我已经知道您可以很长一段时间这样做,但是我从来没有找到过这种技术的用途,在这种技术中,更好的解决方案并没有同时浮现。因此,如果您认为自己有用例,请继续前进。只需在您的评论/文档中清楚一下。特别是这意味着所有涉及的班级的实施必须尊重其不变/假设/等的所有,而不是能够孤立地考虑每个班级,因此您需要确保任何在涉及任何代码上工作的人都知道这一点!

好吧,不打折开始时要警告的问题。但这在某些情况下可能很有用。

首先,我要查看这篇文章的原因是因为我只是这样做,而__slots__不喜欢它。(是的,我的代码是插槽的有效用例,这是纯记忆优化),我试图解决插槽问题。

我首先在Alex Martelli的Python食谱(第一版)中看到了这一点。在第三版中,它是食谱8.19"实施状态对象或状态机器问题"。python的知识渊博的来源。

假设您有一个活跃的对象,其行为与非活性元素不同,并且需要在它们之间快速切换。也许甚至是一个deadenemy。

如果不活跃的环境是子类或兄弟姐妹,则可以切换类属性。更确切地说,确切的祖先比与称呼它一致的方法和属性要小。想想Java 接口,或者,正如几个人提到的那样,您的课程需要考虑到此用途。

现在,您仍然必须管理国家过渡规则和其他各种事情。而且,是的,如果您的客户端代码没有期望此行为和您的实例切换行为,那么事情将击中粉丝。

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

没有问题,直到我的__slots__现在阻止它。但是插槽通常是颈部的疼痛。

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

但是要管理提前已知的非常具体的条件?就像客户期望彻底理解的州机器一样?然后,它与魔术很近,随之而来的所有风险。这很优雅。

Python 3关注?测试它以查看是否有效,但食谱使用Python 3 Print(x)语法在示例中,fwiw。

其他答案做得很好讨论为什么仅仅更改__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)

"危险"是多么取决于初始化对象时子类要做什么。只能运行基类的__init__(),它可能完全无法适当初始化,并且由于不可分化的实例属性,以后会失败。

即使没有,对于大多数用例,似乎也是不良的做法。首先只能实例化所需的类。

以下是您可以在不更改 __class__的情况下做同一件事的一种示例。在评论中引用@unutbu:

假设您正在建模细胞自动机。假设每个单元可能在5个阶段之一中。您可以定义5个阶段1,stage2等。假设每个阶段类都有多种方法。

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的相同

加上,它允许您更改您不想更改的某些特殊方法,仅在Cell中覆盖它们。

加上,它以每个中级Python程序员已经理解的方式适用于数据成员,类方法,静态方法等。

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

是的,我使用了阶段属性,但这不是一个缺点 - 这是跟踪当前阶段的明显方法,更好地调试和可读性。

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

这只是执行此操作的多种不同方法之一,而无需更改__class__

在评论中,我提出了建模蜂窝自动机作为动态__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__已经实现了此目的。

  • 传统方式使用goToStage2goToStage3,...切换阶段。

    动态__class__方法不需要这种方法。你刚才重新选择__class__

  • 传统方式使用特殊方法__getattr__委派一些方法调用在 self.current_stage属性。

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

  • 传统方式需要将cell作为论点传递给 Stage.step。这就是cell.goToStageN

    动态__class__方法无需传递任何内容。这我们要处理的对象拥有我们需要的一切。


结论:

可以使两种方式工作。在我可以设想这两个实现的范围内,在我看来,动态__class__实现将是

  • 更简单(无Cell类),

  • 更优雅(没有丑陋的 goToStage2方法您需要编写cell.step(cell)而不是cell.step()),

  • 且易于理解(没有__getattr__,没有其他级别间接)

最新更新