我发现自己处于一种不寻常的情况,我需要在运行时更改类的MRO。
代码:
class A(object):
def __init__(self):
print self.__class__
print "__init__ A"
self.hello()
def hello(self):
print "A hello"
class B(A):
def __init__(self):
super(B, self).__init__()
print "__init__ B"
self.msg_str = "B"
self.hello()
def hello(self):
print "%s hello" % self.msg_str
a = A()
b = B()
正如预期的那样,这失败了,因为 A 的__init__
方法(从 B 调用时)调用 B 的hello
,该 尝试在属性存在之前访问该属性。
问题是我在可以进行的更改中受到限制:
- B 必须子类 A
- A 无法更改
- A 和 B 都需要 hello 方法
- B 在调用超级
__init__
之前无法初始化其他属性
我确实通过在运行时更改 MRO 在概念上解决了这个问题。简而言之,在 B 的__init__
期间,但在调用超级__init__
之前,MRO 将被更改,以便首先搜索 A 的方法,从而调用 A 的hello
而不是 B(因此失败)。
问题是 MRO 是只读的(在类运行时)。
有没有另一种方法来实现这一点?或者可能完全不同的解决方案(仍然尊重上述约束)?
如果您不受问题中提到的约束,建议使用其他提供的答案。否则,我们需要进入 mro 黑客和元类领域。
经过一些阅读,我发现您可以使用元类更改类的 mro。
但是,这是在类创建时,而不是在对象创建时。需要稍作修改。
元类提供了我们重载的mro
方法,该方法在类创建期间调用(元类的__new__
调用)以生成__mro__
属性。
__mro__
属性不是普通属性,因为:
- 它是只读的
- 它是在元类的
__new__
调用之前定义的
但是,当类的基数更改时,它似乎会重新计算(使用mro
方法)。这构成了黑客攻击的基础。
简述:
- 子类(
B
)是使用元类(change_mro_meta
)创建的。此元类提供:- 重载的 mro 方法
- 用于更改
__mro__
属性的类方法 - 一个类属性(
change_mro
)来控制mro行为
如前所述,在类的__init__
中修改类的 mro 不是线程安全的。
以下内容可能会打扰某些观众。建议观众自行决定。
黑客:
class change_mro_meta(type):
def __new__(cls, cls_name, cls_bases, cls_dict):
out_cls = super(change_mro_meta, cls).__new__(cls, cls_name, cls_bases, cls_dict)
out_cls.change_mro = False
out_cls.hack_mro = classmethod(cls.hack_mro)
out_cls.fix_mro = classmethod(cls.fix_mro)
out_cls.recalc_mro = classmethod(cls.recalc_mro)
return out_cls
@staticmethod
def hack_mro(cls):
cls.change_mro = True
cls.recalc_mro()
@staticmethod
def fix_mro(cls):
cls.change_mro = False
cls.recalc_mro()
@staticmethod
def recalc_mro(cls):
# Changing a class' base causes __mro__ recalculation
cls.__bases__ = cls.__bases__ + tuple()
def mro(cls):
default_mro = super(change_mro_meta, cls).mro()
if hasattr(cls, "change_mro") and cls.change_mro:
return default_mro[1:2] + default_mro
else:
return default_mro
class A(object):
def __init__(self):
print "__init__ A"
self.hello()
def hello(self):
print "A hello"
class B(A):
__metaclass__ = change_mro_meta
def __init__(self):
self.hack_mro()
super(B, self).__init__()
self.fix_mro()
print "__init__ B"
self.msg_str = "B"
self.hello()
def hello(self):
print "%s hello" % self.msg_str
a = A()
b = B()
一些注意事项:
hack_mro
、fix_mro
和recalc_mro
方法对于元类是静态方法,但对类是类方法。它这样做,而不是多重继承,因为我想将 mro 代码组合在一起。
mro
方法本身通常返回默认值。在 hack 条件下,它将默认 mro(直接父类)的第二个元素附加到 mro,从而使父类在子类之前首先看到自己的方法。
我不确定这个黑客的可移植性。它已在运行在Windows 7 64位上的64位CPython 2.7.3上进行了测试。
别担心,我相信这不会在某个地方出现在生产代码中。
可能有更大的解决方案,但一个简单的选择是防御性地编写 B 类。例如:
class B(A):
def __init__(self):
super(B, self).__init__()
print "__init__ B"
self.msg_str = "B"
self.hello()
def hello(self):
if not hasattr(self, 'msg_str'):
A.hello(self)
return
print "%s hello" % self.msg_str
具有正则表达式功能的优秀编辑器可以自动插入适当的if not hasattr(self, 'some_flag'):...
行作为 B 中任何方法的第一行。
对于您的特定示例,一种解决方案是给 B 一个class属性来保存默认消息:
class B(A):
msg_str = "default msg"
def __init__(self):
super(B, self).__init__()
print "__init__ B"
self.msg_str = "B"
self.hello()
def hello(self):
print "%s hello" % self.msg_str
通常这会导致混乱,但在这种情况下可能会有所帮助。 如果在设置实例的msg_str
之前调用B.hello
,它将读取 1 类。 设置实例msg_str
后,它将隐藏类 1,以便将来访问self.msg_str
将看到特定于实例的类。
我不太明白为什么在调用超类之前不能设置属性__init__
. 根据实际情况,可能还有其他解决方案。
我的解决方案是请求原谅:
class A(object):
def __init__(self):
print self.__class__
print "__init__ A"
self.hello()
def hello(self):
print "A hello"
class B(A):
def __init__(self):
super(B, self).__init__()
print "__init__ B"
self.msg_str = "B"
self.hello()
def hello(self):
try:
print "%s hello" % self.msg_str
except AttributeError:
pass # or whatever else you want
a = A()
b = B()
或者,如果您不想重构从init调用的方法:
class A(object):
def __init__(self):
print self.__class__
print "__init__ A"
self.hello()
def hello(self):
print "A hello"
class B(A):
def __init__(self):
try:
super(B, self).__init__()
except AttributeError:
pass # or whatever else you want
print "__init__ B"
self.msg_str = "B"
self.hello()
def hello(self):
print "%s hello" % self.msg_str
a = A()
b = B()
我想指出一个解决方案,该解决方案非常特定于您在问题中提供的示例,因此不太可能有所帮助。(但万一它确实有帮助...
你可以绕过hello
的多态性,方法是将其定义为类成员,而不是方法。
class B(A):
def __init__(self):
super(B, self).__init__()
print "__init__ B"
self.msg_str = "B"
self.hello = lambda: print "%s hello" % self.msg_str
self.hello()
(A
保持不变)。
如果出现以下情况,此解决方案将中断:
- 您的子类
B
,需要覆盖子类中的hello
msg_str
在运行__init__
后修改- 可能还有其他几个 例。。。
我找到了一种更改对象的类或重写其 mro 的方法。
最简单的方法是使用type
函数构建一个新类:
def upgrade_class(obj, old_class, new_class):
if obj.__class__ is old_class:
obj.__class__ = new_class
else:
mro = obj.__class__.mro()
def replace(cls):
if cls is old_class:
return new_class
else:
return cls
bases = tuple(map(replace, mro[1:]))
old_base_class = obj.__class__
new_class = type(old_base_class.__name__, bases, dict(old_base_class.__dict__))
obj.__class__ = new_class
我不知道它是否与特定问题相关,但在我看来,像这样动态更改 MRO 在并发程序中可能会有风险,并且如果这些对象中的任何一个被递归创建,肯定会有问题。
我遇到了一个非基于 MRO 的解决方案,具体取决于此代码可能遇到的错误的性质。(请记住,这是迟来的。也许其他人会想要一个不同的答案。
基本上,B 上的每个 hello() 方法都将包装在一个装饰器中。类似的东西
class deferring(object):
def __init__(self, function):
self.function = function
def __get__(self, instance, owner):
# Return an unbound method, or whatever, when called from B.
if instance is None:
return self.function.__get__(None, owner)
else:
# Indicate that an instance is ready via a flag.
# May need to tweak this based on the exact problem.
if hasattr(instance, '_set_up'):
return self.function.__get__(instance, owner)
else:
# Walk the mro manually.
for cls in owner.__mro__:
# Crazy inefficient. Possible to mitigate, but risky.
for name, attr in vars(cls).items():
if attr is self:
break
else:
continue
return getattr(super(cls, instance), name)
else:
raise TypeError
如果你不想走描述符路线,也可以做一些类似的事情
def deferring(function):
def wrapped(self, *args, **kwargs):
if hasattr(self, '_set_up'):
return function(self, *args, **kwargs)
else:
for cls in type(self).__mro__:
for name, attr in vars(cls).items():
if attr is function:
break
else:
continue
return getattr(super(cls, self), name)(*args, **kwargs)
else:
raise TypeError
return wrapped
在super(B, self).__init__()
之前调用self.msg_str = "B"
。
这不是好的做法,可能有更好的方法来解决您的用例。但一般来说,您可以在运行时修改实例__class__
,这将修改实例(而不是类)的 mro。
例如:
class A:
def f(self):
print('a')
class B:
def f(self):
print('b')
b = B()
b.f() # b
b.__class__ = A
b.f() # a
assert type(b) is A
使用您的用例,它看起来像:
class A:
def __init__(self):
self.hello()
def hello(self):
print("A hello")
class B(A):
def __init__(self):
parent_init = super().__init__
# While A.__init__ is called, inheritance is removed
self.__class__ = A
parent_init()
self.__class__ = B # Restore inheritance
self.msg_str = "B"
def hello(self):
print("%s hello" % self.msg_str)
a = A() # A hello
b = B() # A hello (called in A.__init__)
b.hello() # B hello