假设我有一个类,它有一个数字子类。
我可以实例化这个类。然后我可以将其__class__
属性设置为其中一个子类。我已经在类活动对象上有效地将类类型更改为其子类的类型。我可以调用它上面的方法来调用这些方法的子类的版本。
那么,这样做有多危险?这看起来很奇怪,但做这样的事情是错误吗?尽管能够在运行时更改类型,这是否应该完全避免使用该语言的功能?为什么或为什么不呢?
(根据回复,我会发布一个更具体的问题,询问我想做什么,以及是否有更好的选择)。
答案 0 :(得分:22)
以下列出了我能想到的一些事情,这些事情使得这种事情变得危险,从最坏到最不好的粗略顺序:
__init__
方法,因此您可能无法正确初始化所有实例变量(甚至根本没有)。__slots__
,则所有类必须具有相同的插槽。 (如果你有兼容但不同的插槽,它可能看起来最初工作但做了可怕的事情......)__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__
以来的属性已经达到此目的。
传统方式使用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__
,没有额外的等级
间接)