将自我.__ class__设置为其他东西有多危险?

时间:2012-11-08 00:31:34

标签: python python-2.x

假设我有一个类,它有一个数字子类。

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

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

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

8 个答案:

答案 0 :(得分:22)

以下列出了我能想到的一些事情,这些事情使得这种事情变得危险,从最坏到最不好的粗略顺序:

  • 阅读或调试代码的人可能会感到困惑。
  • 您将不会获得正确的__init__方法,因此您可能无法正确初始化所有实例变量(甚至根本没有)。
  • 2.x和3.x之间的差异足够大,可能会让端口感到痛苦。
  • 有一些边缘情况,包括类方法,手工编码描述符,方法解析顺序的钩子等,它们在经典类和新风格类之间是不同的(同样,在2.x和3之间。 x)的
  • 如果您使用__slots__,则所有类必须具有相同的插槽。 (如果你有兼容但不同的插槽,它可能看起来最初工作但做了可怕的事情......)
  • 新式类中的特殊方法定义可能不会更改。 (事实上​​,这将适用于所有当前的Python实现,但它不是记录工作,所以...)
  • 如果您使用__new__,事情将无法像您想象的那样发挥作用。
  • 如果这些类有不同的元类,事情会变得更加混乱。

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

  • 使用工厂动态创建相应类的实例,而不是创建基本实例,然后将其重新组合为派生实例。
  • 使用__new__或其他机制挂钩构造。
  • 重新设计事物,这样你就有了一个带有一些数据驱动行为的类,而不是滥用继承。

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

答案 1 :(得分:15)

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

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

答案 2 :(得分:3)

在任意类上,这种情况极不可能发挥作用,即使存在也非常脆弱。它基本上与从一个类的方法中拉出底层函数对象,并在不是原始类的实例的对象上调用它们相同。这是否有效取决于内部实现细节,并且是非常紧密耦合的一种形式。

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

答案 3 :(得分:2)

好吧,不要忽视一开始就提醒的问题。但它在某些情况下会很有用。

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

我首先在Alex Martelli的Python Cookbook(第1版)中看到了这一点。在第3版中,它的配方是8.19“实现有状态对象或状态机问题”。一个相当知识渊博的来源,Python-wise。

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

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

现在,您仍然需要管理状态转换规则和各种其他事情。并且,是的,如果您的客户端代码不期望这种行为并且您的实例切换行为,那么事情就会受到影响。

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

没问题,直到我刚刚阻止它的__slots__问题。但是一般来说,老虎机是一种痛苦。

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

但要提前管理非常具体的条件?就像客户希望彻底理解的状态机一样?然后它非常接近魔法,伴随着它带来的所有风险。这很优雅。

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

答案 4 :(得分: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)

答案 5 :(得分:0)

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

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

答案 6 :(得分:-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同样如此,但这是完全正常和诡计的事情,不会让任何人感到困惑。

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

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

  

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

是的,我使用了舞台属性,但这并不是一个缺点 - 它是跟踪当前舞台的明显可见方式,更适合调试和可读性。

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

这只是多种不同方式中的一种,无需更改__class__

答案 7 :(得分:-1)

在评论中,我提议将细胞自动机建模为动态__class__的可能用例。让我们试着充实一下这个想法:


使用动态__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__,没有额外的等级 间接)