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

时间:2012-03-02 19:16:23

标签: python inheritance dynamic

This article有一个代码片段,显示__bases__用于动态更改某些Python代码的继承层次结构的代码段,方法是将类添加到它继承的类的现有类集合中。好吧,这很难读,代码可能更清晰:

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

class Person: pass

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

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

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

对于在运行时更改继承层次结构的新样式类和旧样式类之间的一些Googling似乎indicate some incompatibilities。具体来说:"New-style class objects don't support assignment to their bases attribute"

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

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

7 个答案:

答案 0 :(得分:33)

好的,再次,这不是您通常应该做的事情,这仅供参考。

Python在实例对象上查找方法的位置取决于定义该对象的类的__mro__属性( M ethod R esolution O rder属性)。因此,如果我们可以修改__mro__的{​​{1}},我们就会得到理想的行为。类似的东西:

Person

问题是setattr(Person, '__mro__', (Person, Friendly, object)) 是一个只读属性,因此setattr不起作用。也许如果你是一个Python大师,有一种方法可以解决这个问题,但显然我没有达到大师的地位,因为我无法想到它。

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

__mro__

这不做的是修改任何以前创建的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 来电的详细信息不明确,请阅读e-satis' excellent answer on 'What is a metaclass in Python?'

答案 1 :(得分:18)

我也一直在努力解决这个问题,并且对你的解决方案感兴趣,但是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'

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

蝙蝠的权利,动态地搞乱类层次结构的所有警告都是有效的。

但是如果必须这么做的话,显然,有一个黑客可以在"deallocator differs from 'object" issue when modifying the __bases__ attribute左右获得新的样式类。

您可以定义类对象

class Object(object): pass

从内置元类type派生一个类。 就是这样,现在你的新风格类可以毫无问题地修改__bases__

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

答案 4 :(得分:1)

我需要一个解决方案:

  • 适用于Python 2(&gt; = 2.7)和Python 3(&gt; = 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()

答案 5 :(得分:1)

从技术上讲,此方法在运行时不会继承,因为 __mro__ 无法更改。但是我在这里所做的是使用 __getattr__ 来访问某个类的任何属性或方法。 (按照评论前的数字顺序阅读评论,更有意义)

class Sub:
    def __init__(self, f, cls):
        self.f = f
        self.cls = cls

    # 6) this method will pass the self parameter
    # (which is the original class object we passed)
    # and then it will fill in the rest of the arguments
    # using *args and **kwargs
    
    def __call__(self, *args, **kwargs):
        # 7) the multiple try / except statements
        # are for making sure if an attribute was
        # accessed instead of a function, the __call__
        # method will just return the attribute

        try:
            return self.f(self.cls, *args, **kwargs)
        except TypeError:
            try:
                return self.f(*args, **kwargs)
            except TypeError:
                return self.f

# 1) our base class
class S:
    def __init__(self, func):
        self.cls = func

    def __getattr__(self, item):
        # 5) we are wrapping the attribute we get in the Sub class
        # so we can implement the __call__ method there
        # to be able to pass the parameters in the correct order

        return Sub(getattr(self.cls, item), self.cls)



# 2) class we want to inherit from
class L:
    def run(self, s):
        print("run" + s)

# 3) we create an instance of our base class
# and then pass an instance (or just the class object)
# as a parameter to this instance

s = S(L) # 4) in this case, I'm using the class object

s.run("1")

所以这种替换和重定向将模拟我们想要继承的类的继承。它甚至适用于不带任何参数的属性或方法。

答案 6 :(得分: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中的仿函数非常相似。