如何在运行时动态更改实例的基类?


76

本文的摘要显示了__bases__通过将类添加到继承它的类的现有类集合中来动态更改某些Python代码的继承层次结构的用法。好的,这很难看懂,代码可能更清晰:

class Friendly:
    def hello(self):
        print 'Hello'

class Person: pass

p = Person()
Person.__bases__ = (Friendly,)
p.hello()  # prints "Hello"

也就是说,Person它不是从Friendly源级别继承的,而是通过修改__bases__Person类的属性在运行时动态添加此继承关系。但是,如果更改FriendlyPerson成为新的样式类(通过从object继承),则会出现以下错误:

TypeError: __bases__ assignment: 'Friendly' deallocator differs from 'object'

对此进行一些谷歌搜索似乎表明新样式类和旧样式类在运行时更改继承层次结构方面存在一些不兼容性。具体来说:“新型类对象不支持对其属性的赋值”

我的问题是,是否可以使用Python 2.7+中的新型类,使上面的Friendly / Person示例工作,可能是通过使用__mro__属性?

免责声明:我完全意识到这是晦涩的代码。我完全意识到,在实际的生产代码中,这样的技巧往往难以理解,这纯粹是一个思想实验,并且让人们从中学习有关Python如何处理与多重继承相关的问题的知识。


如果学习者不熟悉metaclass,type(),...,也很高兴阅读此书slideshare.net/gwiener/metaclasses-in-python :)
hkoosha 2014年

这是我的用例。我导入有B类从类继承A.库
FutureNerd

2
这是我的实际用例。我正在导入一个库,该库具有从类A继承的类B。我想使用new_A_method()创建从A继承的New_A。现在,我想创建从...继承的New_B,就像从B继承一样,就像B从New_A继承一样,以便B的方法,A的方法和new_A_method()都可用于New_B的实例。如何在没有猴子修补现有A类的情况下做到这一点?
FutureNerd

您不能New_B同时从B和继承New_A吗?请记住,Python支持多重继承。
亚当·帕金

2
经过一番谷歌搜索之后,以下python错误报告似乎很相关... bugs.python.org/issue672115
mgilson

Answers:


39

好的,同样,这不是您通常应该执行的操作,仅用于提供信息。

其中的Python长相对于一个实例对象的方法是由确定__mro__它定义了对象(该类的属性中号ethod ř esolution ö刻申属性)。因此,如果我们能够改变__mro__Person,我们会得到期望的行为。就像是:

setattr(Person, '__mro__', (Person, Friendly, object))

问题是这__mro__是一个只读属性,因此setattr将不起作用。也许如果您是Python专家,那么可以采取一些措施,但是显然我没有达到专家地位,因为我想不到。

一个可能的解决方法是简单地重新定义该类:

def modify_Person_to_be_friendly():
    # so that we're modifying the global identifier 'Person'
    global Person

    # now just redefine the class using type(), specifying that the new
    # class should inherit from Friendly and have all attributes from
    # our old Person class
    Person = type('Person', (Friendly,), dict(Person.__dict__)) 

def main():
    modify_Person_to_be_friendly()
    p = Person()
    p.hello()  # works!

这不做的是修改任何先前创建的Person实例以使用该hello()方法。例如(仅修改main()):

def main():
    oldperson = Person()
    ModifyPersonToBeFriendly()
    p = Person()
    p.hello()  
    # works!  But:
    oldperson.hello()
    # does not

如果type调用的细节不清楚,请阅读e-satis关于“ Python中的元类是什么?”的出色答案。


-1:为什么仅在您可以通过调用以下方式保存原始的__mro__时才迫使新类从Frielndly继承:(type('Person', (Friendly) + Person.__mro__, dict(Person.__dict__)) 更好的是,添加保护措施,以使Friendly不会在此结束两次)这里还有其他问题-至于“ Person”类的实际定义和使用位置:您的函数仅在当前模块上进行更改-其他运行Person的模块将不受影响-您最好在定义Person的模块上执行monkeypatch。(甚至还有问题)
jsbueno

10
-1首先,您完全错过了例外的原因。Person.__class__.__bases__只要Personobject 直接继承,您就可以在Python 2和3中愉快地进行修改。请参阅下面的akaRem和Sam Gulve的答案。此解决方法只能解决您自己对问题的误解。
卡尔·史密斯

2
这是一个非常有问题的“解决方案”。在此代码的其他问题中,它破坏了__dict__所得对象的属性
user2357112支持Monica19的

24

我也一直在为此而苦苦挣扎,并且对您的解决方案很感兴趣,但是Python 3却将其从我们手中夺走了:

AttributeError: attribute '__dict__' of 'type' objects is not writable

实际上,我确实需要一个装饰器来替换装饰类的(单个)超类。它需要太长的描述才能包含在此处(我尝试过,但无法达到合理的长度和有限的复杂度-它是在许多基于Python的企业服务器的Python应用程序使用上下文的情况下出现的不同的应用程序需要一些代码的稍有不同的变体。)

本页上的讨论和其他类似的讨论提供了提示,__bases__仅在未定义超类的类(即,其唯一的超类是对象)中发生分配问题。通过定义需要替换其超类的类作为琐碎类的子类,我能够解决此问题(对于python 2.7和3.2)。

## T is used so that the other classes are not direct subclasses of object,
## since classes whose base is object don't allow assignment to their __bases__ attribute.

class T: pass

class A(T):
    def __init__(self):
        print('Creating instance of {}'.format(self.__class__.__name__))

## ordinary inheritance
class B(A): pass

## dynamically specified inheritance
class C(T): pass

A()                 # -> Creating instance of A
B()                 # -> Creating instance of B
C.__bases__ = (A,)
C()                 # -> Creating instance of C

## attempt at dynamically specified inheritance starting with a direct subclass
## of object doesn't work
class D: pass

D.__bases__ = (A,)
D()

## Result is:
##     TypeError: __bases__ assignment: 'A' deallocator differs from 'object'

6

我无法保证后果,但是这段代码可以满足您在py2.7.2上的要求。

class Friendly(object):
    def hello(self):
        print 'Hello'

class Person(object): pass

# we can't change the original classes, so we replace them
class newFriendly: pass
newFriendly.__dict__ = dict(Friendly.__dict__)
Friendly = newFriendly
class newPerson: pass
newPerson.__dict__ = dict(Person.__dict__)
Person = newPerson

p = Person()
Person.__bases__ = (Friendly,)
p.hello()  # prints "Hello"

我们知道这是可能的。凉。但是我们永远不会使用它!


3

蝙蝠右翼,所有与类层次结构进行动态混乱的警告都有效。

但是,如果必须这样做,那么显然,"deallocator differs from 'object" issue when modifying the __bases__ attribute对于新样式类来说,这是有问题的。

您可以定义一个类对象

class Object(object): pass

从内置的metaclass派生一个类type。就是这样,现在您的新样式类可以__bases__毫无问题地修改了。

在我的测试中,这实际上非常有效,因为它的所有现有实例(更改继承之前)及其派生类都感受到了更改的影响,包括对其进行mro更新。


2

我需要以下解决方案:

  • 适用于Python 2(> = 2.7)和Python 3(> = 3.2)。
  • 在动态导入依赖项之后,可以更改类库。
  • 让类库从单元测试代码中更改。
  • 与具有自定义元类的类型一起使用。
  • 仍然允许unittest.mock.patch按预期运行。

这是我想出的:

def ensure_class_bases_begin_with(namespace, class_name, base_class):
    """ Ensure the named class's bases start with the base class.

        :param namespace: The namespace containing the class name.
        :param class_name: The name of the class to alter.
        :param base_class: The type to be the first base class for the
            newly created type.
        :return: ``None``.

        Call this function after ensuring `base_class` is
        available, before using the class named by `class_name`.

        """
    existing_class = namespace[class_name]
    assert isinstance(existing_class, type)

    bases = list(existing_class.__bases__)
    if base_class is bases[0]:
        # Already bound to a type with the right bases.
        return
    bases.insert(0, base_class)

    new_class_namespace = existing_class.__dict__.copy()
    # Type creation will assign the correct ‘__dict__’ attribute.
    del new_class_namespace['__dict__']

    metaclass = existing_class.__metaclass__
    new_class = metaclass(class_name, tuple(bases), new_class_namespace)

    namespace[class_name] = new_class

在应用程序中像这样使用:

# foo.py

# Type `Bar` is not available at first, so can't inherit from it yet.
class Foo(object):
    __metaclass__ = type

    def __init__(self):
        self.frob = "spam"

    def __unicode__(self): return "Foo"

# … later …
import bar
ensure_class_bases_begin_with(
        namespace=globals(),
        class_name=str('Foo'),   # `str` type differs on Python 2 vs. 3.
        base_class=bar.Bar)

从单元测试代码中这样使用:

# test_foo.py

""" Unit test for `foo` module. """

import unittest
import mock

import foo
import bar

ensure_class_bases_begin_with(
        namespace=foo.__dict__,
        class_name=str('Foo'),   # `str` type differs on Python 2 vs. 3.
        base_class=bar.Bar)


class Foo_TestCase(unittest.TestCase):
    """ Test cases for `Foo` class. """

    def setUp(self):
        patcher_unicode = mock.patch.object(
                foo.Foo, '__unicode__')
        patcher_unicode.start()
        self.addCleanup(patcher_unicode.stop)

        self.test_instance = foo.Foo()

        patcher_frob = mock.patch.object(
                self.test_instance, 'frob')
        patcher_frob.start()
        self.addCleanup(patcher_frob.stop)

    def test_instantiate(self):
        """ Should create an instance of `Foo`. """
        instance = foo.Foo()

0

如果您需要在运行时更改现有的类,则以上答案很好。但是,如果您只是想创建一个可以被其他类继承的新类,则有一个更干净的解决方案。我从https://stackoverflow.com/a/21060094/3533440获得了这个想法,但是我认为下面的示例更好地说明了一个合法的用例。

def make_default(Map, default_default=None):
    """Returns a class which behaves identically to the given
    Map class, except it gives a default value for unknown keys."""
    class DefaultMap(Map):
        def __init__(self, default=default_default, **kwargs):
            self._default = default
            super().__init__(**kwargs)

        def __missing__(self, key):
            return self._default

    return DefaultMap

DefaultDict = make_default(dict, default_default='wug')

d = DefaultDict(a=1, b=2)
assert d['a'] is 1
assert d['b'] is 2
assert d['c'] is 'wug'

如果我错了,请纠正我,但是这种策略对我来说似乎很容易理解,可以在生产代码中使用它。这与OCaml中的函子非常相似。


1
并不是很确定这与问题有什么关系,因为问题实际上是关于在运行时动态更改基类的。无论如何,与Map直接继承和根据需要重写方法相比,这种方法有什么优势(与标准类似collections.defaultdict)?就目前而言,它make_default只能返回一种类型的东西,那么为什么不仅仅制作DefaultMap顶级标识符,而不必调用make_default以使类实例化呢?
亚当·帕金

感谢您的反馈!(1)我尝试进行动态继承时就落在了这里,所以我想我可以帮助遵循相同道路的人。(2)我相信人们可以make_default用来创建某种其他类型的类似字典的类(Map)的默认版本。您不能Map直接继承,因为Map直到运行时才定义。在这种情况下,您可能想dict直接从中继承,但是我们设想在某些情况下,您可能直到运行时才知道从哪个类的字典类继承。
fredcallaway
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.