15

我发现自己处于一种不寻常的情况,我需要在运行时更改类的 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()

正如所料,这失败了,因为__init__A 的方法(当从 B 调用时)调用 B 的方法hello,它试图在属性存在之前访问它。

问题是我可以做出的改变受到限制:

  • B 必须是 A 的子类
  • A 不能改变
  • A 和 B 都需要一个 hello 方法
  • B在调用super之前不能初始化其他属性__init__

我确实通过在运行时更改 MRO 从概念上解决了这个问题。简而言之,在 B's 期间__init__,但在调用 super 之前__init__,将更改 MRO,以便首先搜索 A 的方法,从而调用 A'shello而不是 B's(因此失败)。

问题是 MRO 是只读的(在类运行时)。

还有其他方法可以实现吗?或者可能完全不同的解决方案(仍然尊重上述限制)?

4

9 回答 9

22

如果您不受问题中提到的约束条件的约束,建议您提供其他答案。否则,我们需要踏上 mro hacks 和元类领域的旅程。

经过一番阅读,我发现您可以使用元类更改类的 mro

然而,这是在类创建时,而不是在对象创建时。稍作修改是必要的。

元类提供了mro我们重载的方法,该方法在类创建(元类的__new__调用)期间调用以生成__mro__属性。

__mro__属性不是普通属性,因为:

  1. 它是只读的
  2. 它是在元类调用之前定义的__new__

mro但是,当类的基数发生变化时,它似乎会被重新计算(使用该方法)。这构成了黑客攻击的基础。

简单来说:

  • 子类 ( B) 是使用元类 ( change_mro_meta) 创建的。这个元类提供:
    • 重载的 mro 方法
    • 更改__mro__属性的类方法
    • change_mro用于控制 mro 行为的类属性 ( )

如前所述,修改类的 mro不是__init__线程安全的。

以下内容可能会打扰部分观众。建议观众酌情决定。

黑客:

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,但类的类方法。它这样做了,而不是多重继承,因为我想将 mro 代码组合在一起。fix_mrorecalc_mro

mro方法本身通常返回默认值。在hack条件下,它将默认mro(直接父类)的第二个元素附加到mro,从而导致父类在子类之前首先看到自己的方法。

我不确定这个黑客的可移植性。它已在 Windows 7 64 位上运行的 64 位 CPython 2.7.3 上进行了测试。

别担心,我敢肯定这不会出现在某个地方的生产代码中。

于 2013-12-30T02:43:40.660 回答
2

可能有更宏大的解决方案,但一个简单的选择是防御性地编写 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 中任何方法的第一行。

于 2013-12-29T06:58:36.610 回答
1

对于您的特定示例,一种解决方案是给 B 一个属性来保存默认消息:

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

通常这会导致混乱,但在这种情况下它可能会有所帮助。如果B.hello在设置实例之前调用msg_str,它将读取第一类。一旦msg_str设置了实例,它就会隐藏第一个类,以便将来的访问self.msg_str将看到特定于实例的类。

我不太明白为什么在调用超类之前不能设置属性__init__。根据具体情况,可能还有其他解决方案。

于 2013-12-29T07:48:35.610 回答
1

我的解决方案是请求宽恕

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()
于 2013-12-29T10:56:51.913 回答
1

我想指出一个解决方案,该解决方案非常适合您在问题中提出的示例,因此不太可能提供帮助。(但万一它确实有帮助......)

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__运行后修改
  • 可能还有其他几种情况...
于 2013-12-29T11:15:39.747 回答
0

我不知道它是否与特定问题有关,但在我看来,像这样动态更改 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
于 2015-03-13T18:21:16.040 回答
0

这不是好的做法,可能有更好的方法来解决您的用例。但一般情况下,可以__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
于 2022-01-24T11:32:29.800 回答
0

我找到了一种方法来更改对象的类或重写它的 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
于 2019-09-06T10:14:23.053 回答
0

打电话self.msg_str = "B"之前super(B, self).__init__()

于 2019-03-19T09:12:04.620 回答