Подтвердить что ты не робот

Как динамически изменять базовый класс экземпляров во время выполнения?

В этой статье есть фрагмент, демонстрирующий использование __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. Однако если вы измените Friendly и Person на новые классы стилей (наследуя от объекта), вы получите следующую ошибку:

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

Немного Googling по этому вопросу, кажется, указывает на некоторые несовместимости между классами нового стиля и старого стиля в отношении изменения иерархии наследования во время выполнения. В частности: "класс объектов нового типа не поддерживают присвоение атрибутов их баз".

Мой вопрос, возможно ли заставить приведенный выше пример Friendly/Person работать с использованием классов нового стиля в Python 2. 7+, возможно, с помощью атрибута __mro__?

Отказ от ответственности: я полностью понимаю, что это неясный код. Я полностью осознаю, что в реальном производственном коде уловки, подобные этому, имеют тенденцию граничить с нечитаемым, это чисто мысленный эксперимент, и для фанатов можно узнать кое-что о том, как Python решает проблемы, связанные с множественным наследованием.

4b9b3361

Ответ 1

Хорошо, опять же, это не то, что вы обычно должны делать, это только для информационных целей.

Где Python ищет метод для объекта экземпляра, определяется атрибутом __mro__ класса, который определяет этот объект ( M этад R. > O rder). Таким образом, если бы мы могли изменить __mro__ of Person, мы получили бы желаемое поведение. Что-то вроде:

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

Проблема заключается в том, что __mro__ является атрибутом readonly, и поэтому setattr не будет работать. Может быть, если вы - гуру-питон, там есть что-то вроде этого, но, очевидно, мне не хватает статуса гуру, о котором я не могу думать.

Возможным обходным путем является просто переопределить класс:

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?" .

Ответ 2

Я тоже борюсь с этим и был заинтригован вашим решением, но 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'

Ответ 3

Я не могу ручаться за последствия, но этот код делает то, что вы хотите в 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"

Мы знаем, что это возможно. Круто. Но мы никогда не будем использовать его!

Ответ 4

Право на летучую мышь, все оговорки о беспорядке с иерархией классов динамически действуют.

Но если это нужно сделать, то, по-видимому, есть хак, который обходит вокруг "deallocator differs from 'object" issue when modifying the __bases__ attribute для новых классов стиля.

Вы можете определить объект класса

class Object(object): pass

Получает класс из встроенного метакласса type. Что бы это ни было, теперь ваши новые классы стиля могут изменять __bases__ без каких-либо проблем.

В моих тестах это действительно работало очень хорошо, как и все существующие (до изменения наследования) экземпляры его и его производные классы ощущали эффект изменения, включая их обновление mro.

Ответ 5

Мне понадобилось решение для этого:

  • Работает как с Python 2 ( >= 2.7), так и с Python 3 ( >= 3.2).
  • Позволяет изменить базы классов после динамического импорта зависимости.
  • Позволяет заменить базы классов из unit test кода.
  • Работает с типами, которые имеют пользовательский метакласс.
  • Все еще позволяет unittest.mock.patch функционировать должным образом.

Вот что я придумал:

def ensure_class_bases_begin_with(namespace, class_name, base_class):
    """ Ensure the named class 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)

Используйте этот код из unit test кода:

# 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()

Ответ 6

Вышеуказанные ответы хороши, если вам нужно изменить существующий класс во время выполнения. Однако, если вы просто хотите создать новый класс, который наследуется каким-то другим классом, существует гораздо более чистое решение. Я получил эту идею от fooobar.com/info/99023/..., но я думаю, что пример ниже лучше иллюстрирует законный вариант использования.

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.